##// END OF EJS Templates
merge upstream
Aras Pranckevicius -
r1861:3f5be4db merge beta
parent child Browse files
Show More
@@ -1,150 +1,149 b''
1 =================================================
1 ========================
2 Welcome to RhodeCode (RhodiumCode) documentation!
2 RhodeCode documentation!
3 =================================================
3 ========================
4
4
5 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
5 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
6 with a built in push/pull server and full text search.
6 with a built in push/pull server and full text search.
7 It works on http/https and has a built in permission/authentication system with
7 It works on http/https and has a built in permission/authentication system with
8 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports
8 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports
9 simple API so it's easy integrable with existing external systems.
9 simple API so it's easy integrable with existing external systems.
10
10
11 RhodeCode is similar in some respects to github or bitbucket_,
11 RhodeCode is similar in some respects to github or bitbucket_,
12 however RhodeCode can be run as standalone hosted application on your own server.
12 however RhodeCode can be run as standalone hosted application on your own server.
13 It is open source and donation ware and focuses more on providing a customized,
13 It is open source and donation ware and focuses more on providing a customized,
14 self administered interface for Mercurial and GIT repositories.
14 self administered interface for Mercurial and GIT repositories.
15 RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to
15 RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to
16 handle multiple different version control systems.
16 handle multiple different version control systems.
17
17
18 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
18 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
19
19
20 RhodeCode demo
20 RhodeCode demo
21 --------------
21 --------------
22
22
23 http://demo.rhodecode.org
23 http://demo.rhodecode.org
24
24
25 The default access is anonymous but you can login to an administrative account
25 The default access is anonymous but you can login to an administrative account
26 using the following credentials:
26 using the following credentials:
27
27
28 - username: demo
28 - username: demo
29 - password: demo12
29 - password: demo12
30
30
31 Source code
31 Source code
32 -----------
32 -----------
33
33
34 The latest sources can be obtained from official RhodeCode instance
34 The latest sources can be obtained from official RhodeCode instance
35 https://secure.rhodecode.org
35 https://secure.rhodecode.org
36
36
37
37
38 MIRRORS:
38 MIRRORS:
39
39
40 Issue tracker and sources at bitbucket_
40 Issue tracker and sources at bitbucket_
41
41
42 http://bitbucket.org/marcinkuzminski/rhodecode
42 http://bitbucket.org/marcinkuzminski/rhodecode
43
43
44 Sources at github_
44 Sources at github_
45
45
46 https://github.com/marcinkuzminski/rhodecode
46 https://github.com/marcinkuzminski/rhodecode
47
47
48 Installation
48 Installation
49 ------------
49 ------------
50
50
51 Please visit http://packages.python.org/RhodeCode/installation.html
51 Please visit http://packages.python.org/RhodeCode/installation.html
52
52
53
53
54 RhodeCode Features
54 RhodeCode Features
55 ------------------
55 ------------------
56
56
57 - Has its own middleware to handle mercurial_ protocol requests.
57 - Has its own middleware to handle mercurial_ protocol requests.
58 Each request can be logged and authenticated.
58 Each request can be logged and authenticated.
59 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
59 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
60 Supports http/https and LDAP
60 Supports http/https and LDAP
61 - Full permissions (private/read/write/admin) and authentication per project.
61 - Full permissions (private/read/write/admin) and authentication per project.
62 One account for web interface and mercurial_ push/pull/clone operations.
62 One account for web interface and mercurial_ push/pull/clone operations.
63 - Have built in users groups for easier permission management
63 - Have built in users groups for easier permission management
64 - Repository groups let you group repos and manage them easier.
64 - Repository groups let you group repos and manage them easier.
65 - Users can fork other users repo. RhodeCode have also compare view to see
65 - Users can fork other users repo. RhodeCode have also compare view to see
66 combined changeset for all changeset made within single push.
66 combined changeset for all changeset made within single push.
67 - Build in commit-api let's you add, edit and commit files right from RhodeCode
67 - Build in commit-api let's you add, edit and commit files right from RhodeCode
68 interface using simple editor or upload form for binaries.
68 interface using simple editor or upload form for binaries.
69 - Mako templates let's you customize the look and feel of the application.
69 - Mako templates let's you customize the look and feel of the application.
70 - Beautiful diffs, annotations and source code browsing all colored by pygments.
70 - Beautiful diffs, annotations and source code browsing all colored by pygments.
71 Raw diffs are made in git-diff format, including git_ binary-patches
71 Raw diffs are made in git-diff format, including git_ binary-patches
72 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
72 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
73 - Admin interface with user/permission management. Admin activity journal, logs
73 - Admin interface with user/permission management. Admin activity journal, logs
74 pulls, pushes, forks, registrations and other actions made by all users.
74 pulls, pushes, forks, registrations and other actions made by all users.
75 - Server side forks. It is possible to fork a project and modify it freely
75 - Server side forks. It is possible to fork a project and modify it freely
76 without breaking the main repository. You can even write Your own hooks
76 without breaking the main repository. You can even write Your own hooks
77 and install them
77 and install them
78 - code review with notification system, inline commenting, all parsed using
78 - code review with notification system, inline commenting, all parsed using
79 rst syntax
79 rst syntax
80 - rst and markdown README support for repositories
80 - rst and markdown README support for repositories
81 - Full text search powered by Whoosh on the source files, and file names.
81 - Full text search powered by Whoosh on the source files, and file names.
82 Build in indexing daemons, with optional incremental index build
82 Build in indexing daemons, with optional incremental index build
83 (no external search servers required all in one application)
83 (no external search servers required all in one application)
84 - Setup project descriptions and info inside built in db for easy, non
84 - Setup project descriptions and info inside built in db for easy, non
85 file-system operations
85 file-system operations
86 - Intelligent cache with invalidation after push or project change, provides
86 - Intelligent cache with invalidation after push or project change, provides
87 high performance and always up to date data.
87 high performance and always up to date data.
88 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
88 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
89 - Async tasks for speed and performance using celery_ (works without them too)
89 - Async tasks for speed and performance using celery_ (works without them too)
90 - Backup scripts can do backup of whole app and send it over scp to desired
90 - Backup scripts can do backup of whole app and send it over scp to desired
91 location
91 location
92 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
92 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
93
93
94
94
95 .. include:: ./docs/screenshots.rst
95 .. include:: ./docs/screenshots.rst
96
96
97
97
98 Incoming / Plans
98 Incoming / Plans
99 ----------------
99 ----------------
100
100
101 - Finer granular permissions per branch, repo group or subrepo
101 - Finer granular permissions per branch, repo group or subrepo
102 - pull requests and web based merges
102 - pull requests and web based merges
103 - per line file history
103 - per line file history
104 - SSH based authentication with server side key management
104 - SSH based authentication with server side key management
105 - Redmine and other bugtrackers integration
106 - Commit based built in wiki system
105 - Commit based built in wiki system
107 - More statistics and graph (global annotation + some more statistics)
106 - More statistics and graph (global annotation + some more statistics)
108 - Other advancements as development continues (or you can of course make
107 - Other advancements as development continues (or you can of course make
109 additions and or requests)
108 additions and or requests)
110
109
111 License
110 License
112 -------
111 -------
113
112
114 ``RhodeCode`` is released under the GPLv3 license.
113 ``RhodeCode`` is released under the GPLv3 license.
115
114
116
115
117 Mailing group Q&A
116 Mailing group Q&A
118 -----------------
117 -----------------
119
118
120 Join the `Google group <http://groups.google.com/group/rhodecode>`_
119 Join the `Google group <http://groups.google.com/group/rhodecode>`_
121
120
122 Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
121 Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
123
122
124 Join #rhodecode on FreeNode (irc.freenode.net)
123 Join #rhodecode on FreeNode (irc.freenode.net)
125 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
124 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
126
125
127 Online documentation
126 Online documentation
128 --------------------
127 --------------------
129
128
130 Online documentation for the current version of RhodeCode is available at
129 Online documentation for the current version of RhodeCode is available at
131 http://packages.python.org/RhodeCode/.
130 http://packages.python.org/RhodeCode/.
132 You may also build the documentation for yourself - go into ``docs/`` and run::
131 You may also build the documentation for yourself - go into ``docs/`` and run::
133
132
134 make html
133 make html
135
134
136 (You need to have sphinx_ installed to build the documentation. If you don't
135 (You need to have sphinx_ installed to build the documentation. If you don't
137 have sphinx_ installed you can install it via the command:
136 have sphinx_ installed you can install it via the command:
138 ``easy_install sphinx``)
137 ``easy_install sphinx``)
139
138
140 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
139 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
141 .. _python: http://www.python.org/
140 .. _python: http://www.python.org/
142 .. _sphinx: http://sphinx.pocoo.org/
141 .. _sphinx: http://sphinx.pocoo.org/
143 .. _mercurial: http://mercurial.selenic.com/
142 .. _mercurial: http://mercurial.selenic.com/
144 .. _bitbucket: http://bitbucket.org/
143 .. _bitbucket: http://bitbucket.org/
145 .. _github: http://github.com/
144 .. _github: http://github.com/
146 .. _subversion: http://subversion.tigris.org/
145 .. _subversion: http://subversion.tigris.org/
147 .. _git: http://git-scm.com/
146 .. _git: http://git-scm.com/
148 .. _celery: http://celeryproject.org/
147 .. _celery: http://celeryproject.org/
149 .. _Sphinx: http://sphinx.pocoo.org/
148 .. _Sphinx: http://sphinx.pocoo.org/
150 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
149 .. _vcs: http://pypi.python.org/pypi/vcs
@@ -1,267 +1,288 b''
1 ################################################################################
1 ################################################################################
2 ################################################################################
2 ################################################################################
3 # RhodeCode - Pylons environment configuration #
3 # RhodeCode - Pylons environment configuration #
4 # #
4 # #
5 # The %(here)s variable will be replaced with the parent directory of this file#
5 # The %(here)s variable will be replaced with the parent directory of this file#
6 ################################################################################
6 ################################################################################
7
7
8 [DEFAULT]
8 [DEFAULT]
9 debug = true
9 debug = true
10 pdebug = false
10 pdebug = false
11 ################################################################################
11 ################################################################################
12 ## Uncomment and replace with the address which should receive ##
12 ## Uncomment and replace with the address which should receive ##
13 ## any error reports after application crash ##
13 ## any error reports after application crash ##
14 ## Additionally those settings will be used by RhodeCode mailing system ##
14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 ################################################################################
15 ################################################################################
16 #email_to = admin@localhost
16 #email_to = admin@localhost
17 #error_email_from = paste_error@localhost
17 #error_email_from = paste_error@localhost
18 #app_email_from = rhodecode-noreply@localhost
18 #app_email_from = rhodecode-noreply@localhost
19 #error_message =
19 #error_message =
20 #email_prefix = [RhodeCode]
20 #email_prefix = [RhodeCode]
21
21
22 #smtp_server = mail.server.com
22 #smtp_server = mail.server.com
23 #smtp_username =
23 #smtp_username =
24 #smtp_password =
24 #smtp_password =
25 #smtp_port =
25 #smtp_port =
26 #smtp_use_tls = false
26 #smtp_use_tls = false
27 #smtp_use_ssl = true
27 #smtp_use_ssl = true
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 #smtp_auth =
29 #smtp_auth =
30
30
31 [server:main]
31 [server:main]
32 ##nr of threads to spawn
32 ##nr of threads to spawn
33 threadpool_workers = 5
33 threadpool_workers = 5
34
34
35 ##max request before thread respawn
35 ##max request before thread respawn
36 threadpool_max_requests = 10
36 threadpool_max_requests = 10
37
37
38 ##option to use threads of process
38 ##option to use threads of process
39 use_threadpool = true
39 use_threadpool = true
40
40
41 use = egg:Paste#http
41 use = egg:Paste#http
42 host = 0.0.0.0
42 host = 0.0.0.0
43 port = 5000
43 port = 5000
44
44
45 [app:main]
45 [app:main]
46 use = egg:rhodecode
46 use = egg:rhodecode
47 full_stack = true
47 full_stack = true
48 static_files = true
48 static_files = true
49 lang=en
49 lang=en
50 cache_dir = %(here)s/data
50 cache_dir = %(here)s/data
51 index_dir = %(here)s/data/index
51 index_dir = %(here)s/data/index
52 app_instance_uuid = develop
52 app_instance_uuid = develop
53 cut_off_limit = 256000
53 cut_off_limit = 256000
54 force_https = false
54 force_https = false
55 commit_parse_limit = 25
55 commit_parse_limit = 25
56 use_gravatar = true
56 use_gravatar = true
57 container_auth_enabled = false
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
58 proxypass_auth_enabled = false
59
59
60 ## overwrite schema of clone url
60 ## overwrite schema of clone url
61 # available vars:
61 ## available vars:
62 # scheme - http/https
62 ## scheme - http/https
63 # user - current user
63 ## user - current user
64 # pass - password
64 ## pass - password
65 # netloc - network location
65 ## netloc - network location
66 # path - usually repo_name
66 ## path - usually repo_name
67 # clone_uri = {scheme}://{user}{pass}{netloc}{path}
67
68 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
69
70 ## issue tracking mapping for commits messages
71 ## uncomment url_pat, issue_server, issue_prefix to enable
72
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #1234
76
77 #url_pat = (?:^#|\s#)(\w+)
78
79 ## server url to the issue, each {id} will be replaced with id
80 ## fetched from the regex
81
82 #issue_server = https://myissueserver.com/issue/{id}
83
84 ## prefix to add to link to indicate it's an url
85 ## #314 will be replaced by <issue_prefix><id>
86
87 #issue_prefix = #
88
68
89
69 ####################################
90 ####################################
70 ### CELERY CONFIG ####
91 ### CELERY CONFIG ####
71 ####################################
92 ####################################
72 use_celery = false
93 use_celery = false
73 broker.host = localhost
94 broker.host = localhost
74 broker.vhost = rabbitmqhost
95 broker.vhost = rabbitmqhost
75 broker.port = 5672
96 broker.port = 5672
76 broker.user = rabbitmq
97 broker.user = rabbitmq
77 broker.password = qweqwe
98 broker.password = qweqwe
78
99
79 celery.imports = rhodecode.lib.celerylib.tasks
100 celery.imports = rhodecode.lib.celerylib.tasks
80
101
81 celery.result.backend = amqp
102 celery.result.backend = amqp
82 celery.result.dburi = amqp://
103 celery.result.dburi = amqp://
83 celery.result.serialier = json
104 celery.result.serialier = json
84
105
85 #celery.send.task.error.emails = true
106 #celery.send.task.error.emails = true
86 #celery.amqp.task.result.expires = 18000
107 #celery.amqp.task.result.expires = 18000
87
108
88 celeryd.concurrency = 2
109 celeryd.concurrency = 2
89 #celeryd.log.file = celeryd.log
110 #celeryd.log.file = celeryd.log
90 celeryd.log.level = debug
111 celeryd.log.level = debug
91 celeryd.max.tasks.per.child = 1
112 celeryd.max.tasks.per.child = 1
92
113
93 #tasks will never be sent to the queue, but executed locally instead.
114 #tasks will never be sent to the queue, but executed locally instead.
94 celery.always.eager = false
115 celery.always.eager = false
95
116
96 ####################################
117 ####################################
97 ### BEAKER CACHE ####
118 ### BEAKER CACHE ####
98 ####################################
119 ####################################
99 beaker.cache.data_dir=%(here)s/data/cache/data
120 beaker.cache.data_dir=%(here)s/data/cache/data
100 beaker.cache.lock_dir=%(here)s/data/cache/lock
121 beaker.cache.lock_dir=%(here)s/data/cache/lock
101
122
102 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
123 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
103
124
104 beaker.cache.super_short_term.type=memory
125 beaker.cache.super_short_term.type=memory
105 beaker.cache.super_short_term.expire=10
126 beaker.cache.super_short_term.expire=10
106 beaker.cache.super_short_term.key_length = 256
127 beaker.cache.super_short_term.key_length = 256
107
128
108 beaker.cache.short_term.type=memory
129 beaker.cache.short_term.type=memory
109 beaker.cache.short_term.expire=60
130 beaker.cache.short_term.expire=60
110 beaker.cache.short_term.key_length = 256
131 beaker.cache.short_term.key_length = 256
111
132
112 beaker.cache.long_term.type=memory
133 beaker.cache.long_term.type=memory
113 beaker.cache.long_term.expire=36000
134 beaker.cache.long_term.expire=36000
114 beaker.cache.long_term.key_length = 256
135 beaker.cache.long_term.key_length = 256
115
136
116 beaker.cache.sql_cache_short.type=memory
137 beaker.cache.sql_cache_short.type=memory
117 beaker.cache.sql_cache_short.expire=10
138 beaker.cache.sql_cache_short.expire=10
118 beaker.cache.sql_cache_short.key_length = 256
139 beaker.cache.sql_cache_short.key_length = 256
119
140
120 beaker.cache.sql_cache_med.type=memory
141 beaker.cache.sql_cache_med.type=memory
121 beaker.cache.sql_cache_med.expire=360
142 beaker.cache.sql_cache_med.expire=360
122 beaker.cache.sql_cache_med.key_length = 256
143 beaker.cache.sql_cache_med.key_length = 256
123
144
124 beaker.cache.sql_cache_long.type=file
145 beaker.cache.sql_cache_long.type=file
125 beaker.cache.sql_cache_long.expire=3600
146 beaker.cache.sql_cache_long.expire=3600
126 beaker.cache.sql_cache_long.key_length = 256
147 beaker.cache.sql_cache_long.key_length = 256
127
148
128 ####################################
149 ####################################
129 ### BEAKER SESSION ####
150 ### BEAKER SESSION ####
130 ####################################
151 ####################################
131 ## Type of storage used for the session, current types are
152 ## Type of storage used for the session, current types are
132 ## dbm, file, memcached, database, and memory.
153 ## dbm, file, memcached, database, and memory.
133 ## The storage uses the Container API
154 ## The storage uses the Container API
134 ## that is also used by the cache system.
155 ## that is also used by the cache system.
135
156
136 ## db session example
157 ## db session example
137
158
138 #beaker.session.type = ext:database
159 #beaker.session.type = ext:database
139 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
160 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
140 #beaker.session.table_name = db_session
161 #beaker.session.table_name = db_session
141
162
142 ## encrypted cookie session, good for many instances
163 ## encrypted cookie session, good for many instances
143 #beaker.session.type = cookie
164 #beaker.session.type = cookie
144
165
145 beaker.session.type = file
166 beaker.session.type = file
146 beaker.session.key = rhodecode
167 beaker.session.key = rhodecode
147 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
168 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
148 #beaker.session.validate_key = 9712sds2212c--zxc123
169 #beaker.session.validate_key = 9712sds2212c--zxc123
149 beaker.session.timeout = 36000
170 beaker.session.timeout = 36000
150 beaker.session.httponly = true
171 beaker.session.httponly = true
151
172
152 ## uncomment for https secure cookie
173 ## uncomment for https secure cookie
153 beaker.session.secure = false
174 beaker.session.secure = false
154
175
155 ##auto save the session to not to use .save()
176 ##auto save the session to not to use .save()
156 beaker.session.auto = False
177 beaker.session.auto = False
157
178
158 ##true exire at browser close
179 ##true exire at browser close
159 #beaker.session.cookie_expires = 3600
180 #beaker.session.cookie_expires = 3600
160
181
161
182
162 ################################################################################
183 ################################################################################
163 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
184 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
164 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
185 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
165 ## execute malicious code after an exception is raised. ##
186 ## execute malicious code after an exception is raised. ##
166 ################################################################################
187 ################################################################################
167 #set debug = false
188 #set debug = false
168
189
169 ##################################
190 ##################################
170 ### LOGVIEW CONFIG ###
191 ### LOGVIEW CONFIG ###
171 ##################################
192 ##################################
172 logview.sqlalchemy = #faa
193 logview.sqlalchemy = #faa
173 logview.pylons.templating = #bfb
194 logview.pylons.templating = #bfb
174 logview.pylons.util = #eee
195 logview.pylons.util = #eee
175
196
176 #########################################################
197 #########################################################
177 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
198 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
178 #########################################################
199 #########################################################
179 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
200 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
180 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
201 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
181 sqlalchemy.db1.echo = false
202 sqlalchemy.db1.echo = false
182 sqlalchemy.db1.pool_recycle = 3600
203 sqlalchemy.db1.pool_recycle = 3600
183 sqlalchemy.convert_unicode = true
204 sqlalchemy.convert_unicode = true
184
205
185 ################################
206 ################################
186 ### LOGGING CONFIGURATION ####
207 ### LOGGING CONFIGURATION ####
187 ################################
208 ################################
188 [loggers]
209 [loggers]
189 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
210 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
190
211
191 [handlers]
212 [handlers]
192 keys = console, console_sql
213 keys = console, console_sql
193
214
194 [formatters]
215 [formatters]
195 keys = generic, color_formatter, color_formatter_sql
216 keys = generic, color_formatter, color_formatter_sql
196
217
197 #############
218 #############
198 ## LOGGERS ##
219 ## LOGGERS ##
199 #############
220 #############
200 [logger_root]
221 [logger_root]
201 level = NOTSET
222 level = NOTSET
202 handlers = console
223 handlers = console
203
224
204 [logger_routes]
225 [logger_routes]
205 level = DEBUG
226 level = DEBUG
206 handlers =
227 handlers =
207 qualname = routes.middleware
228 qualname = routes.middleware
208 # "level = DEBUG" logs the route matched and routing variables.
229 # "level = DEBUG" logs the route matched and routing variables.
209 propagate = 1
230 propagate = 1
210
231
211 [logger_beaker]
232 [logger_beaker]
212 level = DEBUG
233 level = DEBUG
213 handlers =
234 handlers =
214 qualname = beaker.container
235 qualname = beaker.container
215 propagate = 1
236 propagate = 1
216
237
217 [logger_templates]
238 [logger_templates]
218 level = INFO
239 level = INFO
219 handlers =
240 handlers =
220 qualname = pylons.templating
241 qualname = pylons.templating
221 propagate = 1
242 propagate = 1
222
243
223 [logger_rhodecode]
244 [logger_rhodecode]
224 level = DEBUG
245 level = DEBUG
225 handlers =
246 handlers =
226 qualname = rhodecode
247 qualname = rhodecode
227 propagate = 1
248 propagate = 1
228
249
229 [logger_sqlalchemy]
250 [logger_sqlalchemy]
230 level = INFO
251 level = INFO
231 handlers = console_sql
252 handlers = console_sql
232 qualname = sqlalchemy.engine
253 qualname = sqlalchemy.engine
233 propagate = 0
254 propagate = 0
234
255
235 ##############
256 ##############
236 ## HANDLERS ##
257 ## HANDLERS ##
237 ##############
258 ##############
238
259
239 [handler_console]
260 [handler_console]
240 class = StreamHandler
261 class = StreamHandler
241 args = (sys.stderr,)
262 args = (sys.stderr,)
242 level = DEBUG
263 level = DEBUG
243 formatter = color_formatter
264 formatter = color_formatter
244
265
245 [handler_console_sql]
266 [handler_console_sql]
246 class = StreamHandler
267 class = StreamHandler
247 args = (sys.stderr,)
268 args = (sys.stderr,)
248 level = DEBUG
269 level = DEBUG
249 formatter = color_formatter_sql
270 formatter = color_formatter_sql
250
271
251 ################
272 ################
252 ## FORMATTERS ##
273 ## FORMATTERS ##
253 ################
274 ################
254
275
255 [formatter_generic]
276 [formatter_generic]
256 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
277 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
257 datefmt = %Y-%m-%d %H:%M:%S
278 datefmt = %Y-%m-%d %H:%M:%S
258
279
259 [formatter_color_formatter]
280 [formatter_color_formatter]
260 class=rhodecode.lib.colored_formatter.ColorFormatter
281 class=rhodecode.lib.colored_formatter.ColorFormatter
261 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
282 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
262 datefmt = %Y-%m-%d %H:%M:%S
283 datefmt = %Y-%m-%d %H:%M:%S
263
284
264 [formatter_color_formatter_sql]
285 [formatter_color_formatter_sql]
265 class=rhodecode.lib.colored_formatter.ColorFormatterSql
286 class=rhodecode.lib.colored_formatter.ColorFormatterSql
266 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
287 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
267 datefmt = %Y-%m-%d %H:%M:%S
288 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,421 +1,472 b''
1 .. _api:
1 .. _api:
2
2
3
3
4 API
4 API
5 ===
5 ===
6
6
7
7
8 Starting from RhodeCode version 1.2 a simple API was implemented.
8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 There's a single schema for calling all api methods. API is implemented
9 There's a single schema for calling all api methods. API is implemented
10 with JSON protocol both ways. An url to send API request in RhodeCode is
10 with JSON protocol both ways. An url to send API request in RhodeCode is
11 <your_server>/_admin/api
11 <your_server>/_admin/api
12
12
13 API ACCESS FOR WEB VIEWS
14 ++++++++++++++++++++++++
13
15
14 API access can also be turned on for each view decorated with `@LoginRequired`
16 API access can also be turned on for each view decorated with `@LoginRequired`
15 decorator. To enable API access simple change standard login decorator into
17 decorator. To enable API access simple change standard login decorator into
16 `@LoginRequired(api_access=True)`. After such a change view can be accessed
18 `@LoginRequired(api_access=True)`. After such a change view can be accessed
17 by adding a GET parameter to url `?api_key=<api_key>`. By default it's only
19 by adding a GET parameter to url `?api_key=<api_key>`. By default it's only
18 enabled on RSS/ATOM feed views.
20 enabled on RSS/ATOM feed views.
19
21
20
22
23 API ACCESS
24 ++++++++++
25
21 All clients are required to send JSON-RPC spec JSON data::
26 All clients are required to send JSON-RPC spec JSON data::
22
27
23 {
28 {
24 "id:<id>,
29 "id:<id>,
25 "api_key":"<api_key>",
30 "api_key":"<api_key>",
26 "method":"<method_name>",
31 "method":"<method_name>",
27 "args":{"<arg_key>":"<arg_val>"}
32 "args":{"<arg_key>":"<arg_val>"}
28 }
33 }
29
34
30 Example call for autopulling remotes repos using curl::
35 Example call for autopulling remotes repos using curl::
31 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
36 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
32
37
33 Simply provide
38 Simply provide
34 - *id* A value of any type, which is used to match the response with the request that it is replying to.
39 - *id* A value of any type, which is used to match the response with the request that it is replying to.
35 - *api_key* for access and permission validation.
40 - *api_key* for access and permission validation.
36 - *method* is name of method to call
41 - *method* is name of method to call
37 - *args* is an key:value list of arguments to pass to method
42 - *args* is an key:value list of arguments to pass to method
38
43
39 .. note::
44 .. note::
40
45
41 api_key can be found in your user account page
46 api_key can be found in your user account page
42
47
43
48
44 RhodeCode API will return always a JSON-RPC response::
49 RhodeCode API will return always a JSON-RPC response::
45
50
46 {
51 {
47 "id":<id>,
52 "id":<id>,
48 "result": "<result>",
53 "result": "<result>",
49 "error": null
54 "error": null
50 }
55 }
51
56
52 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
57 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
53 calling api *error* key from response will contain failure description
58 calling api *error* key from response will contain failure description
54 and result will be null.
59 and result will be null.
55
60
56 API METHODS
61 API METHODS
57 +++++++++++
62 +++++++++++
58
63
59
64
60 pull
65 pull
61 ----
66 ----
62
67
63 Pulls given repo from remote location. Can be used to automatically keep
68 Pulls given repo from remote location. Can be used to automatically keep
64 remote repos up to date. This command can be executed only using api_key
69 remote repos up to date. This command can be executed only using api_key
65 belonging to user with admin rights
70 belonging to user with admin rights
66
71
67 INPUT::
72 INPUT::
68
73
69 api_key : "<api_key>"
74 api_key : "<api_key>"
70 method : "pull"
75 method : "pull"
71 args : {
76 args : {
72 "repo" : "<repo_name>"
77 "repo_name" : "<reponame>"
73 }
78 }
74
79
75 OUTPUT::
80 OUTPUT::
76
81
77 result : "Pulled from <repo_name>"
82 result : "Pulled from <reponame>"
78 error : null
83 error : null
79
84
80
85
86 get_user
87 --------
88
89 Get's an user by username, Returns empty result if user is not found.
90 This command can be executed only using api_key belonging to user with admin
91 rights.
92
93 INPUT::
94
95 api_key : "<api_key>"
96 method : "get_user"
97 args : {
98 "username" : "<username>"
99 }
100
101 OUTPUT::
102
103 result: None if user does not exist or
104 {
105 "id" : "<id>",
106 "username" : "<username>",
107 "firstname": "<firstname>",
108 "lastname" : "<lastname>",
109 "email" : "<email>",
110 "active" : "<bool>",
111 "admin" :  "<bool>",
112 "ldap" : "<ldap_dn>"
113 }
114
115 error: null
116
117
81 get_users
118 get_users
82 ---------
119 ---------
83
120
84 Lists all existing users. This command can be executed only using api_key
121 Lists all existing users. This command can be executed only using api_key
85 belonging to user with admin rights.
122 belonging to user with admin rights.
86
123
87 INPUT::
124 INPUT::
88
125
89 api_key : "<api_key>"
126 api_key : "<api_key>"
90 method : "get_users"
127 method : "get_users"
91 args : { }
128 args : { }
92
129
93 OUTPUT::
130 OUTPUT::
94
131
95 result: [
132 result: [
96 {
133 {
97 "id" : "<id>",
134 "id" : "<id>",
98 "username" : "<username>",
135 "username" : "<username>",
99 "firstname": "<firstname>",
136 "firstname": "<firstname>",
100 "lastname" : "<lastname>",
137 "lastname" : "<lastname>",
101 "email" : "<email>",
138 "email" : "<email>",
102 "active" : "<bool>",
139 "active" : "<bool>",
103 "admin" :  "<bool>",
140 "admin" :  "<bool>",
104 "ldap" : "<ldap_dn>"
141 "ldap" : "<ldap_dn>"
105 },
142 },
106
143
107 ]
144 ]
108 error: null
145 error: null
109
146
110 create_user
147 create_user
111 -----------
148 -----------
112
149
113 Creates new user in RhodeCode. This command can be executed only using api_key
150 Creates new user in RhodeCode. This command can be executed only using api_key
114 belonging to user with admin rights.
151 belonging to user with admin rights.
115
152
116 INPUT::
153 INPUT::
117
154
118 api_key : "<api_key>"
155 api_key : "<api_key>"
119 method : "create_user"
156 method : "create_user"
120 args : {
157 args : {
121 "username" : "<username>",
158 "username" : "<username>",
122 "password" : "<password>",
159 "password" : "<password>",
123 "firstname" : "<firstname>",
160 "firstname" : "<firstname>",
124 "lastname" : "<lastname>",
161 "lastname" : "<lastname>",
125 "email" : "<useremail>"
162 "email" : "<useremail>"
126 "active" : "<bool> = True",
163 "active" : "<bool> = True",
127 "admin" : "<bool> = False",
164 "admin" : "<bool> = False",
128 "ldap_dn" : "<ldap_dn> = None"
165 "ldap_dn" : "<ldap_dn> = None"
129 }
166 }
130
167
131 OUTPUT::
168 OUTPUT::
132
169
133 result: {
170 result: {
171 "id" : "<new_user_id>",
134 "msg" : "created new user <username>"
172 "msg" : "created new user <username>"
135 }
173 }
136 error: null
174 error: null
137
175
138 get_users_groups
139 ----------------
140
141 Lists all existing users groups. This command can be executed only using api_key
142 belonging to user with admin rights.
143
144 INPUT::
145
146 api_key : "<api_key>"
147 method : "get_users_groups"
148 args : { }
149
150 OUTPUT::
151
152 result : [
153 {
154 "id" : "<id>",
155 "name" : "<name>",
156 "active": "<bool>",
157 "members" : [
158 {
159 "id" : "<userid>",
160 "username" : "<username>",
161 "firstname": "<firstname>",
162 "lastname" : "<lastname>",
163 "email" : "<email>",
164 "active" : "<bool>",
165 "admin" :  "<bool>",
166 "ldap" : "<ldap_dn>"
167 },
168
169 ]
170 }
171 ]
172 error : null
173
174 get_users_group
176 get_users_group
175 ---------------
177 ---------------
176
178
177 Gets an existing users group. This command can be executed only using api_key
179 Gets an existing users group. This command can be executed only using api_key
178 belonging to user with admin rights.
180 belonging to user with admin rights.
179
181
180 INPUT::
182 INPUT::
181
183
182 api_key : "<api_key>"
184 api_key : "<api_key>"
183 method : "get_users_group"
185 method : "get_users_group"
184 args : {
186 args : {
185 "group_name" : "<name>"
187 "group_name" : "<name>"
186 }
188 }
187
189
188 OUTPUT::
190 OUTPUT::
189
191
190 result : None if group not exist
192 result : None if group not exist
191 {
193 {
192 "id" : "<id>",
194 "id" : "<id>",
193 "name" : "<name>",
195 "group_name" : "<groupname>",
194 "active": "<bool>",
196 "active": "<bool>",
195 "members" : [
197 "members" : [
196 { "id" : "<userid>",
198 { "id" : "<userid>",
197 "username" : "<username>",
199 "username" : "<username>",
198 "firstname": "<firstname>",
200 "firstname": "<firstname>",
199 "lastname" : "<lastname>",
201 "lastname" : "<lastname>",
200 "email" : "<email>",
202 "email" : "<email>",
201 "active" : "<bool>",
203 "active" : "<bool>",
202 "admin" :  "<bool>",
204 "admin" :  "<bool>",
203 "ldap" : "<ldap_dn>"
205 "ldap" : "<ldap_dn>"
204 },
206 },
205
207
206 ]
208 ]
207 }
209 }
208 error : null
210 error : null
209
211
212 get_users_groups
213 ----------------
214
215 Lists all existing users groups. This command can be executed only using
216 api_key belonging to user with admin rights.
217
218 INPUT::
219
220 api_key : "<api_key>"
221 method : "get_users_groups"
222 args : { }
223
224 OUTPUT::
225
226 result : [
227 {
228 "id" : "<id>",
229 "group_name" : "<groupname>",
230 "active": "<bool>",
231 "members" : [
232 {
233 "id" : "<userid>",
234 "username" : "<username>",
235 "firstname": "<firstname>",
236 "lastname" : "<lastname>",
237 "email" : "<email>",
238 "active" : "<bool>",
239 "admin" :  "<bool>",
240 "ldap" : "<ldap_dn>"
241 },
242
243 ]
244 }
245 ]
246 error : null
247
248
210 create_users_group
249 create_users_group
211 ------------------
250 ------------------
212
251
213 Creates new users group. This command can be executed only using api_key
252 Creates new users group. This command can be executed only using api_key
214 belonging to user with admin rights
253 belonging to user with admin rights
215
254
216 INPUT::
255 INPUT::
217
256
218 api_key : "<api_key>"
257 api_key : "<api_key>"
219 method : "create_users_group"
258 method : "create_users_group"
220 args: {
259 args: {
221 "name": "<name>",
260 "group_name": "<groupname>",
222 "active":"<bool> = True"
261 "active":"<bool> = True"
223 }
262 }
224
263
225 OUTPUT::
264 OUTPUT::
226
265
227 result: {
266 result: {
228 "id": "<newusersgroupid>",
267 "id": "<newusersgroupid>",
229 "msg": "created new users group <name>"
268 "msg": "created new users group <groupname>"
230 }
269 }
231 error: null
270 error: null
232
271
233 add_user_to_users_group
272 add_user_to_users_group
234 -----------------------
273 -----------------------
235
274
236 Adds a user to a users group. This command can be executed only using api_key
275 Adds a user to a users group. This command can be executed only using api_key
237 belonging to user with admin rights
276 belonging to user with admin rights
238
277
239 INPUT::
278 INPUT::
240
279
241 api_key : "<api_key>"
280 api_key : "<api_key>"
242 method : "add_user_users_group"
281 method : "add_user_users_group"
243 args: {
282 args: {
244 "group_name" : "<groupname>",
283 "group_name" : "<groupname>",
245 "username" : "<username>"
284 "username" : "<username>"
246 }
285 }
247
286
248 OUTPUT::
287 OUTPUT::
249
288
250 result: {
289 result: {
251 "id": "<newusersgroupmemberid>",
290 "id": "<newusersgroupmemberid>",
252 "msg": "created new users group member"
291 "msg": "created new users group member"
253 }
292 }
254 error: null
293 error: null
255
294
295 get_repo
296 --------
297
298 Gets an existing repository. This command can be executed only using api_key
299 belonging to user with admin rights
300
301 INPUT::
302
303 api_key : "<api_key>"
304 method : "get_repo"
305 args: {
306 "repo_name" : "<reponame>"
307 }
308
309 OUTPUT::
310
311 result: None if repository does not exist or
312 {
313 "id" : "<id>",
314 "repo_name" : "<reponame>"
315 "type" : "<type>",
316 "description" : "<description>",
317 "members" : [
318 { "id" : "<userid>",
319 "username" : "<username>",
320 "firstname": "<firstname>",
321 "lastname" : "<lastname>",
322 "email" : "<email>",
323 "active" : "<bool>",
324 "admin" :  "<bool>",
325 "ldap" : "<ldap_dn>",
326 "permission" : "repository.(read|write|admin)"
327 },
328
329 {
330 "id" : "<usersgroupid>",
331 "name" : "<usersgroupname>",
332 "active": "<bool>",
333 "permission" : "repository.(read|write|admin)"
334 },
335
336 ]
337 }
338 error: null
339
256 get_repos
340 get_repos
257 ---------
341 ---------
258
342
259 Lists all existing repositories. This command can be executed only using api_key
343 Lists all existing repositories. This command can be executed only using api_key
260 belonging to user with admin rights
344 belonging to user with admin rights
261
345
262 INPUT::
346 INPUT::
263
347
264 api_key : "<api_key>"
348 api_key : "<api_key>"
265 method : "get_repos"
349 method : "get_repos"
266 args: { }
350 args: { }
267
351
268 OUTPUT::
352 OUTPUT::
269
353
270 result: [
354 result: [
271 {
355 {
272 "id" : "<id>",
356 "id" : "<id>",
273 "name" : "<name>"
357 "repo_name" : "<reponame>"
274 "type" : "<type>",
358 "type" : "<type>",
275 "description" : "<description>"
359 "description" : "<description>"
276 },
360 },
277
361
278 ]
362 ]
279 error: null
363 error: null
280
364
281 get_repo
282 --------
283
284 Gets an existing repository. This command can be executed only using api_key
285 belonging to user with admin rights
286
287 INPUT::
288
289 api_key : "<api_key>"
290 method : "get_repo"
291 args: {
292 "name" : "<name>"
293 }
294
295 OUTPUT::
296
297 result: None if repository not exist
298 {
299 "id" : "<id>",
300 "name" : "<name>"
301 "type" : "<type>",
302 "description" : "<description>",
303 "members" : [
304 { "id" : "<userid>",
305 "username" : "<username>",
306 "firstname": "<firstname>",
307 "lastname" : "<lastname>",
308 "email" : "<email>",
309 "active" : "<bool>",
310 "admin" :  "<bool>",
311 "ldap" : "<ldap_dn>",
312 "permission" : "repository.(read|write|admin)"
313 },
314
315 {
316 "id" : "<usersgroupid>",
317 "name" : "<usersgroupname>",
318 "active": "<bool>",
319 "permission" : "repository.(read|write|admin)"
320 },
321
322 ]
323 }
324 error: null
325
365
326 get_repo_nodes
366 get_repo_nodes
327 --------------
367 --------------
328
368
329 returns a list of nodes and it's children in a flat list for a given path
369 returns a list of nodes and it's children in a flat list for a given path
330 at given revision. It's possible to specify ret_type to show only files or
370 at given revision. It's possible to specify ret_type to show only `files` or
331 dirs. This command can be executed only using api_key belonging to user
371 `dirs`. This command can be executed only using api_key belonging to user
332 with admin rights
372 with admin rights
333
373
334 INPUT::
374 INPUT::
335
375
336 api_key : "<api_key>"
376 api_key : "<api_key>"
337 method : "get_repo_nodes"
377 method : "get_repo_nodes"
338 args: {
378 args: {
339 "repo_name" : "<name>",
379 "repo_name" : "<reponame>",
340 "revision" : "<revision>",
380 "revision" : "<revision>",
341 "root_path" : "<root_path>",
381 "root_path" : "<root_path>",
342 "ret_type" : "<ret_type>" = 'all'
382 "ret_type" : "<ret_type>" = 'all'
343 }
383 }
344
384
345 OUTPUT::
385 OUTPUT::
346
386
347 result: [
387 result: [
348 {
388 {
349 "name" : "<name>"
389 "name" : "<name>"
350 "type" : "<type>",
390 "type" : "<type>",
351 },
391 },
352
392
353 ]
393 ]
354 error: null
394 error: null
355
395
356
396
357
397
358 create_repo
398 create_repo
359 -----------
399 -----------
360
400
361 Creates a repository. This command can be executed only using api_key
401 Creates a repository. This command can be executed only using api_key
362 belonging to user with admin rights.
402 belonging to user with admin rights.
363 If repository name contains "/", all needed repository groups will be created.
403 If repository name contains "/", all needed repository groups will be created.
364 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
404 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
365 and create "baz" repository with "bar" as group.
405 and create "baz" repository with "bar" as group.
366
406
367 INPUT::
407 INPUT::
368
408
369 api_key : "<api_key>"
409 api_key : "<api_key>"
370 method : "create_repo"
410 method : "create_repo"
371 args: {
411 args: {
372 "name" : "<name>",
412 "repo_name" : "<reponame>",
373 "owner_name" : "<ownername>",
413 "owner_name" : "<ownername>",
374 "description" : "<description> = ''",
414 "description" : "<description> = ''",
375 "repo_type" : "<type> = 'hg'",
415 "repo_type" : "<type> = 'hg'",
376 "private" : "<bool> = False"
416 "private" : "<bool> = False"
377 }
417 }
378
418
379 OUTPUT::
419 OUTPUT::
380
420
381 result: None
421 result: {
422 "id": "<newrepoid>",
423 "msg": "Created new repository <reponame>",
424 }
382 error: null
425 error: null
383
426
384 add_user_to_repo
427 add_user_to_repo
385 ----------------
428 ----------------
386
429
387 Add a user to a repository. This command can be executed only using api_key
430 Add a user to a repository. This command can be executed only using api_key
388 belonging to user with admin rights.
431 belonging to user with admin rights.
389 If "perm" is None, user will be removed from the repository.
432 If "perm" is None, user will be removed from the repository.
390
433
391 INPUT::
434 INPUT::
392
435
393 api_key : "<api_key>"
436 api_key : "<api_key>"
394 method : "add_user_to_repo"
437 method : "add_user_to_repo"
395 args: {
438 args: {
396 "repo_name" : "<reponame>",
439 "repo_name" : "<reponame>",
397 "username" : "<username>",
440 "username" : "<username>",
398 "perm" : "(None|repository.(read|write|admin))",
441 "perm" : "(None|repository.(read|write|admin))",
399 }
442 }
400
443
401 OUTPUT::
444 OUTPUT::
402
445
403 result: None
446 result: {
447 "msg" : "Added perm: <perm> for <username> in repo: <reponame>"
448 }
404 error: null
449 error: null
405
450
406 add_users_group_to_repo
451 add_users_group_to_repo
407 -----------------------
452 -----------------------
408
453
409 Add a users group to a repository. This command can be executed only using
454 Add a users group to a repository. This command can be executed only using
410 api_key belonging to user with admin rights. If "perm" is None, group will
455 api_key belonging to user with admin rights. If "perm" is None, group will
411 be removed from the repository.
456 be removed from the repository.
412
457
413 INPUT::
458 INPUT::
414
459
415 api_key : "<api_key>"
460 api_key : "<api_key>"
416 method : "add_users_group_to_repo"
461 method : "add_users_group_to_repo"
417 args: {
462 args: {
418 "repo_name" : "<reponame>",
463 "repo_name" : "<reponame>",
419 "group_name" : "<groupname>",
464 "group_name" : "<groupname>",
420 "perm" : "(None|repository.(read|write|admin))",
465 "perm" : "(None|repository.(read|write|admin))",
421 } No newline at end of file
466 }
467 OUTPUT::
468
469 result: {
470 "msg" : Added perm: <perm> for <groupname> in repo: <reponame>"
471 }
472
@@ -1,10 +1,10 b''
1 .. _api:
1 .. _indexapi:
2
2
3 API Reference
3 API Reference
4 =============
4 =============
5
5
6 .. toctree::
6 .. toctree::
7 :maxdepth: 3
7 :maxdepth: 3
8
8
9 models
9 models
10 api No newline at end of file
10 api
@@ -1,19 +1,34 b''
1 .. _models:
1 .. _models:
2
2
3 The :mod:`models` Module
3 The :mod:`models` Module
4 ========================
4 ========================
5
5
6 .. automodule:: rhodecode.model
6 .. automodule:: rhodecode.model
7 :members:
7 :members:
8
8
9 .. automodule:: rhodecode.model.comment
10 :members:
11
12 .. automodule:: rhodecode.model.notification
13 :members:
14
9 .. automodule:: rhodecode.model.permission
15 .. automodule:: rhodecode.model.permission
10 :members:
16 :members:
11
17
18 .. automodule:: rhodecode.model.repo_permission
19 :members:
20
12 .. automodule:: rhodecode.model.repo
21 .. automodule:: rhodecode.model.repo
13 :members:
22 :members:
14
23
24 .. automodule:: rhodecode.model.repos_group
25 :members:
26
15 .. automodule:: rhodecode.model.scm
27 .. automodule:: rhodecode.model.scm
16 :members:
28 :members:
17
29
18 .. automodule:: rhodecode.model.user
30 .. automodule:: rhodecode.model.user
19 :members:
31 :members:
32
33 .. automodule:: rhodecode.model.users_group
34 :members: No newline at end of file
@@ -1,441 +1,443 b''
1 .. _changelog:
1 .. _changelog:
2
2
3 Changelog
3 Changelog
4 =========
4 =========
5
5
6
6
7 1.3.0 (**XXXX-XX-XX**)
7 1.3.0 (**XXXX-XX-XX**)
8 ======================
8 ======================
9
9
10 :status: in-progress
10 :status: in-progress
11 :branch: beta
11 :branch: beta
12
12
13 news
13 news
14 ----
14 ----
15
15
16 - code review, inspired by github code-comments
16 - code review, inspired by github code-comments
17 - #215 rst and markdown README files support
17 - #215 rst and markdown README files support
18 - #252 Container-based and proxy pass-through authentication support
18 - #252 Container-based and proxy pass-through authentication support
19 - #44 branch browser. Filtering of changelog by branches
19 - #44 branch browser. Filtering of changelog by branches
20 - mercurial bookmarks support
20 - mercurial bookmarks support
21 - hover top menu
21 - hover top menu
22 - configurable clone url template with possibility to specify protocol like
22 - configurable clone url template with possibility to specify protocol like
23 ssh:// or http:// and also manually alter other parts of clone_url.
23 ssh:// or http:// and also manually alter other parts of clone_url.
24 - enabled largefiles extension by default
24 - enabled largefiles extension by default
25 - optimized summary file pages and saved a lot of unused space in them
25 - optimized summary file pages and saved a lot of unused space in them
26 - #239 option to manually mark repository as fork
26 - #239 option to manually mark repository as fork
27 - #320 mapping of commit authors to RhodeCode users
27 - #320 mapping of commit authors to RhodeCode users
28 - #304 hashes are displayed using monospace font
28 - #304 hashes are displayed using monospace font
29 - diff configuration, toggle white lines and context lines
29 - diff configuration, toggle white lines and context lines
30 - #307 configurable diffs, whitespace toggle, increasing context lines
30 - #307 configurable diffs, whitespace toggle, increasing context lines
31 - sorting on branches, tags and bookmarks using YUI datatable
31 - sorting on branches, tags and bookmarks using YUI datatable
32 - improved file filter on files page
32 - improved file filter on files page
33 - implements #330 api method for listing nodes ar particular revision
33 - implements #330 api method for listing nodes ar particular revision
34 - fixed #331 RhodeCode mangles repository names if the a repository group
34 - fixed #331 RhodeCode mangles repository names if the a repository group
35 contains the "full path" to the repositories
35 contains the "full path" to the repositories
36
36 - #73 added linking issues in commit messages to choosen issue tracker url
37 based on user defined regular expression
38
37 fixes
39 fixes
38 -----
40 -----
39
41
40 - rewrote dbsession management for atomic operations, and better error handling
42 - rewrote dbsession management for atomic operations, and better error handling
41 - fixed sorting of repo tables
43 - fixed sorting of repo tables
42 - #326 escape of special html entities in diffs
44 - #326 escape of special html entities in diffs
43 - normalized user_name => username in api attributes
45 - normalized user_name => username in api attributes
44 - fixes #298 ldap created users with mixed case emails created conflicts
46 - fixes #298 ldap created users with mixed case emails created conflicts
45 on saving a form
47 on saving a form
46 - fixes issue when owner of a repo couldn't revoke permissions for users
48 - fixes issue when owner of a repo couldn't revoke permissions for users
47 and groups
49 and groups
48
50
49 1.2.3 (**2011-11-02**)
51 1.2.3 (**2011-11-02**)
50 ======================
52 ======================
51
53
52 news
54 news
53 ----
55 ----
54
56
55 - added option to manage repos group for non admin users
57 - added option to manage repos group for non admin users
56 - added following API methods for get_users, create_user, get_users_groups,
58 - added following API methods for get_users, create_user, get_users_groups,
57 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
59 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
58 get_repo, create_repo, add_user_to_repo
60 get_repo, create_repo, add_user_to_repo
59 - implements #237 added password confirmation for my account
61 - implements #237 added password confirmation for my account
60 and admin edit user.
62 and admin edit user.
61 - implements #291 email notification for global events are now sent to all
63 - implements #291 email notification for global events are now sent to all
62 administrator users, and global config email.
64 administrator users, and global config email.
63
65
64 fixes
66 fixes
65 -----
67 -----
66
68
67 - added option for passing auth method for smtp mailer
69 - added option for passing auth method for smtp mailer
68 - #276 issue with adding a single user with id>10 to usergroups
70 - #276 issue with adding a single user with id>10 to usergroups
69 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
71 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
70 - #288 fixes managing of repos in a group for non admin user
72 - #288 fixes managing of repos in a group for non admin user
71
73
72 1.2.2 (**2011-10-17**)
74 1.2.2 (**2011-10-17**)
73 ======================
75 ======================
74
76
75 news
77 news
76 ----
78 ----
77
79
78 - #226 repo groups are available by path instead of numerical id
80 - #226 repo groups are available by path instead of numerical id
79
81
80 fixes
82 fixes
81 -----
83 -----
82
84
83 - #259 Groups with the same name but with different parent group
85 - #259 Groups with the same name but with different parent group
84 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
86 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
85 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
87 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
86 - #265 ldap save fails sometimes on converting attributes to booleans,
88 - #265 ldap save fails sometimes on converting attributes to booleans,
87 added getter and setter into model that will prevent from this on db model level
89 added getter and setter into model that will prevent from this on db model level
88 - fixed problems with timestamps issues #251 and #213
90 - fixed problems with timestamps issues #251 and #213
89 - fixes #266 RhodeCode allows to create repo with the same name and in
91 - fixes #266 RhodeCode allows to create repo with the same name and in
90 the same parent as group
92 the same parent as group
91 - fixes #245 Rescan of the repositories on Windows
93 - fixes #245 Rescan of the repositories on Windows
92 - fixes #248 cannot edit repos inside a group on windows
94 - fixes #248 cannot edit repos inside a group on windows
93 - fixes #219 forking problems on windows
95 - fixes #219 forking problems on windows
94
96
95 1.2.1 (**2011-10-08**)
97 1.2.1 (**2011-10-08**)
96 ======================
98 ======================
97
99
98 news
100 news
99 ----
101 ----
100
102
101
103
102 fixes
104 fixes
103 -----
105 -----
104
106
105 - fixed problems with basic auth and push problems
107 - fixed problems with basic auth and push problems
106 - gui fixes
108 - gui fixes
107 - fixed logger
109 - fixed logger
108
110
109 1.2.0 (**2011-10-07**)
111 1.2.0 (**2011-10-07**)
110 ======================
112 ======================
111
113
112 news
114 news
113 ----
115 ----
114
116
115 - implemented #47 repository groups
117 - implemented #47 repository groups
116 - implemented #89 Can setup google analytics code from settings menu
118 - implemented #89 Can setup google analytics code from settings menu
117 - implemented #91 added nicer looking archive urls with more download options
119 - implemented #91 added nicer looking archive urls with more download options
118 like tags, branches
120 like tags, branches
119 - implemented #44 into file browsing, and added follow branch option
121 - implemented #44 into file browsing, and added follow branch option
120 - implemented #84 downloads can be enabled/disabled for each repository
122 - implemented #84 downloads can be enabled/disabled for each repository
121 - anonymous repository can be cloned without having to pass default:default
123 - anonymous repository can be cloned without having to pass default:default
122 into clone url
124 into clone url
123 - fixed #90 whoosh indexer can index chooses repositories passed in command
125 - fixed #90 whoosh indexer can index chooses repositories passed in command
124 line
126 line
125 - extended journal with day aggregates and paging
127 - extended journal with day aggregates and paging
126 - implemented #107 source code lines highlight ranges
128 - implemented #107 source code lines highlight ranges
127 - implemented #93 customizable changelog on combined revision ranges -
129 - implemented #93 customizable changelog on combined revision ranges -
128 equivalent of githubs compare view
130 equivalent of githubs compare view
129 - implemented #108 extended and more powerful LDAP configuration
131 - implemented #108 extended and more powerful LDAP configuration
130 - implemented #56 users groups
132 - implemented #56 users groups
131 - major code rewrites optimized codes for speed and memory usage
133 - major code rewrites optimized codes for speed and memory usage
132 - raw and diff downloads are now in git format
134 - raw and diff downloads are now in git format
133 - setup command checks for write access to given path
135 - setup command checks for write access to given path
134 - fixed many issues with international characters and unicode. It uses utf8
136 - fixed many issues with international characters and unicode. It uses utf8
135 decode with replace to provide less errors even with non utf8 encoded strings
137 decode with replace to provide less errors even with non utf8 encoded strings
136 - #125 added API KEY access to feeds
138 - #125 added API KEY access to feeds
137 - #109 Repository can be created from external Mercurial link (aka. remote
139 - #109 Repository can be created from external Mercurial link (aka. remote
138 repository, and manually updated (via pull) from admin panel
140 repository, and manually updated (via pull) from admin panel
139 - beta git support - push/pull server + basic view for git repos
141 - beta git support - push/pull server + basic view for git repos
140 - added followers page and forks page
142 - added followers page and forks page
141 - server side file creation (with binary file upload interface)
143 - server side file creation (with binary file upload interface)
142 and edition with commits powered by codemirror
144 and edition with commits powered by codemirror
143 - #111 file browser file finder, quick lookup files on whole file tree
145 - #111 file browser file finder, quick lookup files on whole file tree
144 - added quick login sliding menu into main page
146 - added quick login sliding menu into main page
145 - changelog uses lazy loading of affected files details, in some scenarios
147 - changelog uses lazy loading of affected files details, in some scenarios
146 this can improve speed of changelog page dramatically especially for
148 this can improve speed of changelog page dramatically especially for
147 larger repositories.
149 larger repositories.
148 - implements #214 added support for downloading subrepos in download menu.
150 - implements #214 added support for downloading subrepos in download menu.
149 - Added basic API for direct operations on rhodecode via JSON
151 - Added basic API for direct operations on rhodecode via JSON
150 - Implemented advanced hook management
152 - Implemented advanced hook management
151
153
152 fixes
154 fixes
153 -----
155 -----
154
156
155 - fixed file browser bug, when switching into given form revision the url was
157 - fixed file browser bug, when switching into given form revision the url was
156 not changing
158 not changing
157 - fixed propagation to error controller on simplehg and simplegit middlewares
159 - fixed propagation to error controller on simplehg and simplegit middlewares
158 - fixed error when trying to make a download on empty repository
160 - fixed error when trying to make a download on empty repository
159 - fixed problem with '[' chars in commit messages in journal
161 - fixed problem with '[' chars in commit messages in journal
160 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
162 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
161 - journal fork fixes
163 - journal fork fixes
162 - removed issue with space inside renamed repository after deletion
164 - removed issue with space inside renamed repository after deletion
163 - fixed strange issue on formencode imports
165 - fixed strange issue on formencode imports
164 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
166 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
165 - #150 fixes for errors on repositories mapped in db but corrupted in
167 - #150 fixes for errors on repositories mapped in db but corrupted in
166 filesystem
168 filesystem
167 - fixed problem with ascendant characters in realm #181
169 - fixed problem with ascendant characters in realm #181
168 - fixed problem with sqlite file based database connection pool
170 - fixed problem with sqlite file based database connection pool
169 - whoosh indexer and code stats share the same dynamic extensions map
171 - whoosh indexer and code stats share the same dynamic extensions map
170 - fixes #188 - relationship delete of repo_to_perm entry on user removal
172 - fixes #188 - relationship delete of repo_to_perm entry on user removal
171 - fixes issue #189 Trending source files shows "show more" when no more exist
173 - fixes issue #189 Trending source files shows "show more" when no more exist
172 - fixes issue #197 Relative paths for pidlocks
174 - fixes issue #197 Relative paths for pidlocks
173 - fixes issue #198 password will require only 3 chars now for login form
175 - fixes issue #198 password will require only 3 chars now for login form
174 - fixes issue #199 wrong redirection for non admin users after creating a repository
176 - fixes issue #199 wrong redirection for non admin users after creating a repository
175 - fixes issues #202, bad db constraint made impossible to attach same group
177 - fixes issues #202, bad db constraint made impossible to attach same group
176 more than one time. Affects only mysql/postgres
178 more than one time. Affects only mysql/postgres
177 - fixes #218 os.kill patch for windows was missing sig param
179 - fixes #218 os.kill patch for windows was missing sig param
178 - improved rendering of dag (they are not trimmed anymore when number of
180 - improved rendering of dag (they are not trimmed anymore when number of
179 heads exceeds 5)
181 heads exceeds 5)
180
182
181 1.1.8 (**2011-04-12**)
183 1.1.8 (**2011-04-12**)
182 ======================
184 ======================
183
185
184 news
186 news
185 ----
187 ----
186
188
187 - improved windows support
189 - improved windows support
188
190
189 fixes
191 fixes
190 -----
192 -----
191
193
192 - fixed #140 freeze of python dateutil library, since new version is python2.x
194 - fixed #140 freeze of python dateutil library, since new version is python2.x
193 incompatible
195 incompatible
194 - setup-app will check for write permission in given path
196 - setup-app will check for write permission in given path
195 - cleaned up license info issue #149
197 - cleaned up license info issue #149
196 - fixes for issues #137,#116 and problems with unicode and accented characters.
198 - fixes for issues #137,#116 and problems with unicode and accented characters.
197 - fixes crashes on gravatar, when passed in email as unicode
199 - fixes crashes on gravatar, when passed in email as unicode
198 - fixed tooltip flickering problems
200 - fixed tooltip flickering problems
199 - fixed came_from redirection on windows
201 - fixed came_from redirection on windows
200 - fixed logging modules, and sql formatters
202 - fixed logging modules, and sql formatters
201 - windows fixes for os.kill issue #133
203 - windows fixes for os.kill issue #133
202 - fixes path splitting for windows issues #148
204 - fixes path splitting for windows issues #148
203 - fixed issue #143 wrong import on migration to 1.1.X
205 - fixed issue #143 wrong import on migration to 1.1.X
204 - fixed problems with displaying binary files, thanks to Thomas Waldmann
206 - fixed problems with displaying binary files, thanks to Thomas Waldmann
205 - removed name from archive files since it's breaking ui for long repo names
207 - removed name from archive files since it's breaking ui for long repo names
206 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
208 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
207 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
209 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
208 Thomas Waldmann
210 Thomas Waldmann
209 - fixed issue #166 summary pager was skipping 10 revisions on second page
211 - fixed issue #166 summary pager was skipping 10 revisions on second page
210
212
211
213
212 1.1.7 (**2011-03-23**)
214 1.1.7 (**2011-03-23**)
213 ======================
215 ======================
214
216
215 news
217 news
216 ----
218 ----
217
219
218 fixes
220 fixes
219 -----
221 -----
220
222
221 - fixed (again) #136 installation support for FreeBSD
223 - fixed (again) #136 installation support for FreeBSD
222
224
223
225
224 1.1.6 (**2011-03-21**)
226 1.1.6 (**2011-03-21**)
225 ======================
227 ======================
226
228
227 news
229 news
228 ----
230 ----
229
231
230 fixes
232 fixes
231 -----
233 -----
232
234
233 - fixed #136 installation support for FreeBSD
235 - fixed #136 installation support for FreeBSD
234 - RhodeCode will check for python version during installation
236 - RhodeCode will check for python version during installation
235
237
236 1.1.5 (**2011-03-17**)
238 1.1.5 (**2011-03-17**)
237 ======================
239 ======================
238
240
239 news
241 news
240 ----
242 ----
241
243
242 - basic windows support, by exchanging pybcrypt into sha256 for windows only
244 - basic windows support, by exchanging pybcrypt into sha256 for windows only
243 highly inspired by idea of mantis406
245 highly inspired by idea of mantis406
244
246
245 fixes
247 fixes
246 -----
248 -----
247
249
248 - fixed sorting by author in main page
250 - fixed sorting by author in main page
249 - fixed crashes with diffs on binary files
251 - fixed crashes with diffs on binary files
250 - fixed #131 problem with boolean values for LDAP
252 - fixed #131 problem with boolean values for LDAP
251 - fixed #122 mysql problems thanks to striker69
253 - fixed #122 mysql problems thanks to striker69
252 - fixed problem with errors on calling raw/raw_files/annotate functions
254 - fixed problem with errors on calling raw/raw_files/annotate functions
253 with unknown revisions
255 with unknown revisions
254 - fixed returned rawfiles attachment names with international character
256 - fixed returned rawfiles attachment names with international character
255 - cleaned out docs, big thanks to Jason Harris
257 - cleaned out docs, big thanks to Jason Harris
256
258
257 1.1.4 (**2011-02-19**)
259 1.1.4 (**2011-02-19**)
258 ======================
260 ======================
259
261
260 news
262 news
261 ----
263 ----
262
264
263 fixes
265 fixes
264 -----
266 -----
265
267
266 - fixed formencode import problem on settings page, that caused server crash
268 - fixed formencode import problem on settings page, that caused server crash
267 when that page was accessed as first after server start
269 when that page was accessed as first after server start
268 - journal fixes
270 - journal fixes
269 - fixed option to access repository just by entering http://server/<repo_name>
271 - fixed option to access repository just by entering http://server/<repo_name>
270
272
271 1.1.3 (**2011-02-16**)
273 1.1.3 (**2011-02-16**)
272 ======================
274 ======================
273
275
274 news
276 news
275 ----
277 ----
276
278
277 - implemented #102 allowing the '.' character in username
279 - implemented #102 allowing the '.' character in username
278 - added option to access repository just by entering http://server/<repo_name>
280 - added option to access repository just by entering http://server/<repo_name>
279 - celery task ignores result for better performance
281 - celery task ignores result for better performance
280
282
281 fixes
283 fixes
282 -----
284 -----
283
285
284 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
286 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
285 apollo13 and Johan Walles
287 apollo13 and Johan Walles
286 - small fixes in journal
288 - small fixes in journal
287 - fixed problems with getting setting for celery from .ini files
289 - fixed problems with getting setting for celery from .ini files
288 - registration, password reset and login boxes share the same title as main
290 - registration, password reset and login boxes share the same title as main
289 application now
291 application now
290 - fixed #113: to high permissions to fork repository
292 - fixed #113: to high permissions to fork repository
291 - fixed problem with '[' chars in commit messages in journal
293 - fixed problem with '[' chars in commit messages in journal
292 - removed issue with space inside renamed repository after deletion
294 - removed issue with space inside renamed repository after deletion
293 - db transaction fixes when filesystem repository creation failed
295 - db transaction fixes when filesystem repository creation failed
294 - fixed #106 relation issues on databases different than sqlite
296 - fixed #106 relation issues on databases different than sqlite
295 - fixed static files paths links to use of url() method
297 - fixed static files paths links to use of url() method
296
298
297 1.1.2 (**2011-01-12**)
299 1.1.2 (**2011-01-12**)
298 ======================
300 ======================
299
301
300 news
302 news
301 ----
303 ----
302
304
303
305
304 fixes
306 fixes
305 -----
307 -----
306
308
307 - fixes #98 protection against float division of percentage stats
309 - fixes #98 protection against float division of percentage stats
308 - fixed graph bug
310 - fixed graph bug
309 - forced webhelpers version since it was making troubles during installation
311 - forced webhelpers version since it was making troubles during installation
310
312
311 1.1.1 (**2011-01-06**)
313 1.1.1 (**2011-01-06**)
312 ======================
314 ======================
313
315
314 news
316 news
315 ----
317 ----
316
318
317 - added force https option into ini files for easier https usage (no need to
319 - added force https option into ini files for easier https usage (no need to
318 set server headers with this options)
320 set server headers with this options)
319 - small css updates
321 - small css updates
320
322
321 fixes
323 fixes
322 -----
324 -----
323
325
324 - fixed #96 redirect loop on files view on repositories without changesets
326 - fixed #96 redirect loop on files view on repositories without changesets
325 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
327 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
326 and server crashed with errors
328 and server crashed with errors
327 - fixed large tooltips problems on main page
329 - fixed large tooltips problems on main page
328 - fixed #92 whoosh indexer is more error proof
330 - fixed #92 whoosh indexer is more error proof
329
331
330 1.1.0 (**2010-12-18**)
332 1.1.0 (**2010-12-18**)
331 ======================
333 ======================
332
334
333 news
335 news
334 ----
336 ----
335
337
336 - rewrite of internals for vcs >=0.1.10
338 - rewrite of internals for vcs >=0.1.10
337 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
339 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
338 with older clients
340 with older clients
339 - anonymous access, authentication via ldap
341 - anonymous access, authentication via ldap
340 - performance upgrade for cached repos list - each repository has its own
342 - performance upgrade for cached repos list - each repository has its own
341 cache that's invalidated when needed.
343 cache that's invalidated when needed.
342 - performance upgrades on repositories with large amount of commits (20K+)
344 - performance upgrades on repositories with large amount of commits (20K+)
343 - main page quick filter for filtering repositories
345 - main page quick filter for filtering repositories
344 - user dashboards with ability to follow chosen repositories actions
346 - user dashboards with ability to follow chosen repositories actions
345 - sends email to admin on new user registration
347 - sends email to admin on new user registration
346 - added cache/statistics reset options into repository settings
348 - added cache/statistics reset options into repository settings
347 - more detailed action logger (based on hooks) with pushed changesets lists
349 - more detailed action logger (based on hooks) with pushed changesets lists
348 and options to disable those hooks from admin panel
350 and options to disable those hooks from admin panel
349 - introduced new enhanced changelog for merges that shows more accurate results
351 - introduced new enhanced changelog for merges that shows more accurate results
350 - new improved and faster code stats (based on pygments lexers mapping tables,
352 - new improved and faster code stats (based on pygments lexers mapping tables,
351 showing up to 10 trending sources for each repository. Additionally stats
353 showing up to 10 trending sources for each repository. Additionally stats
352 can be disabled in repository settings.
354 can be disabled in repository settings.
353 - gui optimizations, fixed application width to 1024px
355 - gui optimizations, fixed application width to 1024px
354 - added cut off (for large files/changesets) limit into config files
356 - added cut off (for large files/changesets) limit into config files
355 - whoosh, celeryd, upgrade moved to paster command
357 - whoosh, celeryd, upgrade moved to paster command
356 - other than sqlite database backends can be used
358 - other than sqlite database backends can be used
357
359
358 fixes
360 fixes
359 -----
361 -----
360
362
361 - fixes #61 forked repo was showing only after cache expired
363 - fixes #61 forked repo was showing only after cache expired
362 - fixes #76 no confirmation on user deletes
364 - fixes #76 no confirmation on user deletes
363 - fixes #66 Name field misspelled
365 - fixes #66 Name field misspelled
364 - fixes #72 block user removal when he owns repositories
366 - fixes #72 block user removal when he owns repositories
365 - fixes #69 added password confirmation fields
367 - fixes #69 added password confirmation fields
366 - fixes #87 RhodeCode crashes occasionally on updating repository owner
368 - fixes #87 RhodeCode crashes occasionally on updating repository owner
367 - fixes #82 broken annotations on files with more than 1 blank line at the end
369 - fixes #82 broken annotations on files with more than 1 blank line at the end
368 - a lot of fixes and tweaks for file browser
370 - a lot of fixes and tweaks for file browser
369 - fixed detached session issues
371 - fixed detached session issues
370 - fixed when user had no repos he would see all repos listed in my account
372 - fixed when user had no repos he would see all repos listed in my account
371 - fixed ui() instance bug when global hgrc settings was loaded for server
373 - fixed ui() instance bug when global hgrc settings was loaded for server
372 instance and all hgrc options were merged with our db ui() object
374 instance and all hgrc options were merged with our db ui() object
373 - numerous small bugfixes
375 - numerous small bugfixes
374
376
375 (special thanks for TkSoh for detailed feedback)
377 (special thanks for TkSoh for detailed feedback)
376
378
377
379
378 1.0.2 (**2010-11-12**)
380 1.0.2 (**2010-11-12**)
379 ======================
381 ======================
380
382
381 news
383 news
382 ----
384 ----
383
385
384 - tested under python2.7
386 - tested under python2.7
385 - bumped sqlalchemy and celery versions
387 - bumped sqlalchemy and celery versions
386
388
387 fixes
389 fixes
388 -----
390 -----
389
391
390 - fixed #59 missing graph.js
392 - fixed #59 missing graph.js
391 - fixed repo_size crash when repository had broken symlinks
393 - fixed repo_size crash when repository had broken symlinks
392 - fixed python2.5 crashes.
394 - fixed python2.5 crashes.
393
395
394
396
395 1.0.1 (**2010-11-10**)
397 1.0.1 (**2010-11-10**)
396 ======================
398 ======================
397
399
398 news
400 news
399 ----
401 ----
400
402
401 - small css updated
403 - small css updated
402
404
403 fixes
405 fixes
404 -----
406 -----
405
407
406 - fixed #53 python2.5 incompatible enumerate calls
408 - fixed #53 python2.5 incompatible enumerate calls
407 - fixed #52 disable mercurial extension for web
409 - fixed #52 disable mercurial extension for web
408 - fixed #51 deleting repositories don't delete it's dependent objects
410 - fixed #51 deleting repositories don't delete it's dependent objects
409
411
410
412
411 1.0.0 (**2010-11-02**)
413 1.0.0 (**2010-11-02**)
412 ======================
414 ======================
413
415
414 - security bugfix simplehg wasn't checking for permissions on commands
416 - security bugfix simplehg wasn't checking for permissions on commands
415 other than pull or push.
417 other than pull or push.
416 - fixed doubled messages after push or pull in admin journal
418 - fixed doubled messages after push or pull in admin journal
417 - templating and css corrections, fixed repo switcher on chrome, updated titles
419 - templating and css corrections, fixed repo switcher on chrome, updated titles
418 - admin menu accessible from options menu on repository view
420 - admin menu accessible from options menu on repository view
419 - permissions cached queries
421 - permissions cached queries
420
422
421 1.0.0rc4 (**2010-10-12**)
423 1.0.0rc4 (**2010-10-12**)
422 ==========================
424 ==========================
423
425
424 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
426 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
425 - removed cache_manager settings from sqlalchemy meta
427 - removed cache_manager settings from sqlalchemy meta
426 - added sqlalchemy cache settings to ini files
428 - added sqlalchemy cache settings to ini files
427 - validated password length and added second try of failure on paster setup-app
429 - validated password length and added second try of failure on paster setup-app
428 - fixed setup database destroy prompt even when there was no db
430 - fixed setup database destroy prompt even when there was no db
429
431
430
432
431 1.0.0rc3 (**2010-10-11**)
433 1.0.0rc3 (**2010-10-11**)
432 =========================
434 =========================
433
435
434 - fixed i18n during installation.
436 - fixed i18n during installation.
435
437
436 1.0.0rc2 (**2010-10-11**)
438 1.0.0rc2 (**2010-10-11**)
437 =========================
439 =========================
438
440
439 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
441 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
440 occure. After vcs is fixed it'll be put back again.
442 occure. After vcs is fixed it'll be put back again.
441 - templating/css rewrites, optimized css. No newline at end of file
443 - templating/css rewrites, optimized css.
@@ -1,59 +1,58 b''
1 .. _index:
1 .. _index:
2
2
3 .. include:: ./../README.rst
3 .. include:: ./../README.rst
4
4
5 Documentation
5 Users Guide
6 -------------
6 -----------
7
7
8 **Installation:**
8 **Installation:**
9
9
10 .. toctree::
10 .. toctree::
11 :maxdepth: 1
11 :maxdepth: 1
12
12
13 installation
13 installation
14 setup
14 setup
15 upgrade
15 upgrade
16
16
17 **Usage**
17 **Usage**
18
18
19 .. toctree::
19 .. toctree::
20 :maxdepth: 1
20 :maxdepth: 1
21
21
22 usage/general
22 usage/general
23 usage/enable_git
23 usage/enable_git
24 usage/statistics
24 usage/statistics
25 usage/backup
25 usage/backup
26 usage/api_key_access
27
26
28 **Develop**
27 **Develop**
29
28
30 .. toctree::
29 .. toctree::
31 :maxdepth: 1
30 :maxdepth: 1
32
31
33 contributing
32 contributing
34 changelog
33 changelog
35
34
36 **API**
35 **API**
37
36
38 .. toctree::
37 .. toctree::
39 :maxdepth: 2
38 :maxdepth: 1
40
39
41 api/index
40 api/index
42
41
43
42
44 Other topics
43 Other topics
45 ------------
44 ------------
46
45
47 * :ref:`genindex`
46 * :ref:`genindex`
48 * :ref:`search`
47 * :ref:`search`
49
48
50 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
49 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
51 .. _python: http://www.python.org/
50 .. _python: http://www.python.org/
52 .. _django: http://www.djangoproject.com/
51 .. _django: http://www.djangoproject.com/
53 .. _mercurial: http://mercurial.selenic.com/
52 .. _mercurial: http://mercurial.selenic.com/
54 .. _bitbucket: http://bitbucket.org/
53 .. _bitbucket: http://bitbucket.org/
55 .. _subversion: http://subversion.tigris.org/
54 .. _subversion: http://subversion.tigris.org/
56 .. _git: http://git-scm.com/
55 .. _git: http://git-scm.com/
57 .. _celery: http://celeryproject.org/
56 .. _celery: http://celeryproject.org/
58 .. _Sphinx: http://sphinx.pocoo.org/
57 .. _Sphinx: http://sphinx.pocoo.org/
59 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
58 .. _vcs: http://pypi.python.org/pypi/vcs
@@ -1,694 +1,712 b''
1 .. _setup:
1 .. _setup:
2
2
3 Setup
3 Setup
4 =====
4 =====
5
5
6
6
7 Setting up RhodeCode
7 Setting up RhodeCode
8 --------------------
8 --------------------
9
9
10 First, you will need to create a RhodeCode configuration file. Run the
10 First, you will need to create a RhodeCode configuration file. Run the
11 following command to do this::
11 following command to do this::
12
12
13 paster make-config RhodeCode production.ini
13 paster make-config RhodeCode production.ini
14
14
15 - This will create the file `production.ini` in the current directory. This
15 - This will create the file `production.ini` in the current directory. This
16 configuration file contains the various settings for RhodeCode, e.g proxy
16 configuration file contains the various settings for RhodeCode, e.g proxy
17 port, email settings, usage of static files, cache, celery settings and
17 port, email settings, usage of static files, cache, celery settings and
18 logging.
18 logging.
19
19
20
20
21 Next, you need to create the databases used by RhodeCode. I recommend that you
21 Next, you need to create the databases used by RhodeCode. I recommend that you
22 use sqlite (default) or postgresql. If you choose a database other than the
22 use sqlite (default) or postgresql. If you choose a database other than the
23 default ensure you properly adjust the db url in your production.ini
23 default ensure you properly adjust the db url in your production.ini
24 configuration file to use this other database. Create the databases by running
24 configuration file to use this other database. Create the databases by running
25 the following command::
25 the following command::
26
26
27 paster setup-app production.ini
27 paster setup-app production.ini
28
28
29 This will prompt you for a "root" path. This "root" path is the location where
29 This will prompt you for a "root" path. This "root" path is the location where
30 RhodeCode will store all of its repositories on the current machine. After
30 RhodeCode will store all of its repositories on the current machine. After
31 entering this "root" path ``setup-app`` will also prompt you for a username
31 entering this "root" path ``setup-app`` will also prompt you for a username
32 and password for the initial admin account which ``setup-app`` sets up for you.
32 and password for the initial admin account which ``setup-app`` sets up for you.
33
33
34 - The ``setup-app`` command will create all of the needed tables and an admin
34 - The ``setup-app`` command will create all of the needed tables and an admin
35 account. When choosing a root path you can either use a new empty location,
35 account. When choosing a root path you can either use a new empty location,
36 or a location which already contains existing repositories. If you choose a
36 or a location which already contains existing repositories. If you choose a
37 location which contains existing repositories RhodeCode will simply add all
37 location which contains existing repositories RhodeCode will simply add all
38 of the repositories at the chosen location to it's database. (Note: make
38 of the repositories at the chosen location to it's database. (Note: make
39 sure you specify the correct path to the root).
39 sure you specify the correct path to the root).
40 - Note: the given path for mercurial_ repositories **must** be write accessible
40 - Note: the given path for mercurial_ repositories **must** be write accessible
41 for the application. It's very important since the RhodeCode web interface
41 for the application. It's very important since the RhodeCode web interface
42 will work without write access, but when trying to do a push it will
42 will work without write access, but when trying to do a push it will
43 eventually fail with permission denied errors unless it has write access.
43 eventually fail with permission denied errors unless it has write access.
44
44
45 You are now ready to use RhodeCode, to run it simply execute::
45 You are now ready to use RhodeCode, to run it simply execute::
46
46
47 paster serve production.ini
47 paster serve production.ini
48
48
49 - This command runs the RhodeCode server. The web app should be available at the
49 - This command runs the RhodeCode server. The web app should be available at the
50 127.0.0.1:5000. This ip and port is configurable via the production.ini
50 127.0.0.1:5000. This ip and port is configurable via the production.ini
51 file created in previous step
51 file created in previous step
52 - Use the admin account you created above when running ``setup-app`` to login
52 - Use the admin account you created above when running ``setup-app`` to login
53 to the web app.
53 to the web app.
54 - The default permissions on each repository is read, and the owner is admin.
54 - The default permissions on each repository is read, and the owner is admin.
55 Remember to update these if needed.
55 Remember to update these if needed.
56 - In the admin panel you can toggle ldap, anonymous, permissions settings. As
56 - In the admin panel you can toggle ldap, anonymous, permissions settings. As
57 well as edit more advanced options on users and repositories
57 well as edit more advanced options on users and repositories
58
58
59 Try copying your own mercurial repository into the "root" directory you are
59 Try copying your own mercurial repository into the "root" directory you are
60 using, then from within the RhodeCode web application choose Admin >
60 using, then from within the RhodeCode web application choose Admin >
61 repositories. Then choose Add New Repository. Add the repository you copied
61 repositories. Then choose Add New Repository. Add the repository you copied
62 into the root. Test that you can browse your repository from within RhodeCode
62 into the root. Test that you can browse your repository from within RhodeCode
63 and then try cloning your repository from RhodeCode with::
63 and then try cloning your repository from RhodeCode with::
64
64
65 hg clone http://127.0.0.1:5000/<repository name>
65 hg clone http://127.0.0.1:5000/<repository name>
66
66
67 where *repository name* is replaced by the name of your repository.
67 where *repository name* is replaced by the name of your repository.
68
68
69 Using RhodeCode with SSH
69 Using RhodeCode with SSH
70 ------------------------
70 ------------------------
71
71
72 RhodeCode currently only hosts repositories using http and https. (The addition
72 RhodeCode currently only hosts repositories using http and https. (The addition
73 of ssh hosting is a planned future feature.) However you can easily use ssh in
73 of ssh hosting is a planned future feature.) However you can easily use ssh in
74 parallel with RhodeCode. (Repository access via ssh is a standard "out of
74 parallel with RhodeCode. (Repository access via ssh is a standard "out of
75 the box" feature of mercurial_ and you can use this to access any of the
75 the box" feature of mercurial_ and you can use this to access any of the
76 repositories that RhodeCode is hosting. See PublishingRepositories_)
76 repositories that RhodeCode is hosting. See PublishingRepositories_)
77
77
78 RhodeCode repository structures are kept in directories with the same name
78 RhodeCode repository structures are kept in directories with the same name
79 as the project. When using repository groups, each group is a subdirectory.
79 as the project. When using repository groups, each group is a subdirectory.
80 This allows you to easily use ssh for accessing repositories.
80 This allows you to easily use ssh for accessing repositories.
81
81
82 In order to use ssh you need to make sure that your web-server and the users
82 In order to use ssh you need to make sure that your web-server and the users
83 login accounts have the correct permissions set on the appropriate directories.
83 login accounts have the correct permissions set on the appropriate directories.
84 (Note that these permissions are independent of any permissions you have set up
84 (Note that these permissions are independent of any permissions you have set up
85 using the RhodeCode web interface.)
85 using the RhodeCode web interface.)
86
86
87 If your main directory (the same as set in RhodeCode settings) is for example
87 If your main directory (the same as set in RhodeCode settings) is for example
88 set to **/home/hg** and the repository you are using is named `rhodecode`, then
88 set to **/home/hg** and the repository you are using is named `rhodecode`, then
89 to clone via ssh you should run::
89 to clone via ssh you should run::
90
90
91 hg clone ssh://user@server.com/home/hg/rhodecode
91 hg clone ssh://user@server.com/home/hg/rhodecode
92
92
93 Using other external tools such as mercurial-server_ or using ssh key based
93 Using other external tools such as mercurial-server_ or using ssh key based
94 authentication is fully supported.
94 authentication is fully supported.
95
95
96 Note: In an advanced setup, in order for your ssh access to use the same
96 Note: In an advanced setup, in order for your ssh access to use the same
97 permissions as set up via the RhodeCode web interface, you can create an
97 permissions as set up via the RhodeCode web interface, you can create an
98 authentication hook to connect to the rhodecode db and runs check functions for
98 authentication hook to connect to the rhodecode db and runs check functions for
99 permissions against that.
99 permissions against that.
100
100
101 Setting up Whoosh full text search
101 Setting up Whoosh full text search
102 ----------------------------------
102 ----------------------------------
103
103
104 Starting from version 1.1 the whoosh index can be build by using the paster
104 Starting from version 1.1 the whoosh index can be build by using the paster
105 command ``make-index``. To use ``make-index`` you must specify the configuration
105 command ``make-index``. To use ``make-index`` you must specify the configuration
106 file that stores the location of the index. You may specify the location of the
106 file that stores the location of the index. You may specify the location of the
107 repositories (`--repo-location`). If not specified, this value is retrieved
107 repositories (`--repo-location`). If not specified, this value is retrieved
108 from the RhodeCode database. This was required prior to 1.2. Starting from
108 from the RhodeCode database. This was required prior to 1.2. Starting from
109 version 1.2 it is also possible to specify a comma separated list of
109 version 1.2 it is also possible to specify a comma separated list of
110 repositories (`--index-only`) to build index only on chooses repositories
110 repositories (`--index-only`) to build index only on chooses repositories
111 skipping any other found in repos location
111 skipping any other found in repos location
112
112
113 You may optionally pass the option `-f` to enable a full index rebuild. Without
113 You may optionally pass the option `-f` to enable a full index rebuild. Without
114 the `-f` option, indexing will run always in "incremental" mode.
114 the `-f` option, indexing will run always in "incremental" mode.
115
115
116 For an incremental index build use::
116 For an incremental index build use::
117
117
118 paster make-index production.ini
118 paster make-index production.ini
119
119
120 For a full index rebuild use::
120 For a full index rebuild use::
121
121
122 paster make-index production.ini -f
122 paster make-index production.ini -f
123
123
124
124
125 building index just for chosen repositories is possible with such command::
125 building index just for chosen repositories is possible with such command::
126
126
127 paster make-index production.ini --index-only=vcs,rhodecode
127 paster make-index production.ini --index-only=vcs,rhodecode
128
128
129
129
130 In order to do periodical index builds and keep your index always up to date.
130 In order to do periodical index builds and keep your index always up to date.
131 It's recommended to do a crontab entry for incremental indexing.
131 It's recommended to do a crontab entry for incremental indexing.
132 An example entry might look like this::
132 An example entry might look like this::
133
133
134 /path/to/python/bin/paster make-index /path/to/rhodecode/production.ini
134 /path/to/python/bin/paster make-index /path/to/rhodecode/production.ini
135
135
136 When using incremental mode (the default) whoosh will check the last
136 When using incremental mode (the default) whoosh will check the last
137 modification date of each file and add it to be reindexed if a newer file is
137 modification date of each file and add it to be reindexed if a newer file is
138 available. The indexing daemon checks for any removed files and removes them
138 available. The indexing daemon checks for any removed files and removes them
139 from index.
139 from index.
140
140
141 If you want to rebuild index from scratch, you can use the `-f` flag as above,
141 If you want to rebuild index from scratch, you can use the `-f` flag as above,
142 or in the admin panel you can check `build from scratch` flag.
142 or in the admin panel you can check `build from scratch` flag.
143
143
144
144
145 Setting up LDAP support
145 Setting up LDAP support
146 -----------------------
146 -----------------------
147
147
148 RhodeCode starting from version 1.1 supports ldap authentication. In order
148 RhodeCode starting from version 1.1 supports ldap authentication. In order
149 to use LDAP, you have to install the python-ldap_ package. This package is
149 to use LDAP, you have to install the python-ldap_ package. This package is
150 available via pypi, so you can install it by running
150 available via pypi, so you can install it by running
151
151
152 using easy_install::
152 using easy_install::
153
153
154 easy_install python-ldap
154 easy_install python-ldap
155
155
156 using pip::
156 using pip::
157
157
158 pip install python-ldap
158 pip install python-ldap
159
159
160 .. note::
160 .. note::
161 python-ldap requires some certain libs on your system, so before installing
161 python-ldap requires some certain libs on your system, so before installing
162 it check that you have at least `openldap`, and `sasl` libraries.
162 it check that you have at least `openldap`, and `sasl` libraries.
163
163
164 LDAP settings are located in admin->ldap section,
164 LDAP settings are located in admin->ldap section,
165
165
166 Here's a typical ldap setup::
166 Here's a typical ldap setup::
167
167
168 Connection settings
168 Connection settings
169 Enable LDAP = checked
169 Enable LDAP = checked
170 Host = host.example.org
170 Host = host.example.org
171 Port = 389
171 Port = 389
172 Account = <account>
172 Account = <account>
173 Password = <password>
173 Password = <password>
174 Connection Security = LDAPS connection
174 Connection Security = LDAPS connection
175 Certificate Checks = DEMAND
175 Certificate Checks = DEMAND
176
176
177 Search settings
177 Search settings
178 Base DN = CN=users,DC=host,DC=example,DC=org
178 Base DN = CN=users,DC=host,DC=example,DC=org
179 LDAP Filter = (&(objectClass=user)(!(objectClass=computer)))
179 LDAP Filter = (&(objectClass=user)(!(objectClass=computer)))
180 LDAP Search Scope = SUBTREE
180 LDAP Search Scope = SUBTREE
181
181
182 Attribute mappings
182 Attribute mappings
183 Login Attribute = uid
183 Login Attribute = uid
184 First Name Attribute = firstName
184 First Name Attribute = firstName
185 Last Name Attribute = lastName
185 Last Name Attribute = lastName
186 E-mail Attribute = mail
186 E-mail Attribute = mail
187
187
188 .. _enable_ldap:
188 .. _enable_ldap:
189
189
190 Enable LDAP : required
190 Enable LDAP : required
191 Whether to use LDAP for authenticating users.
191 Whether to use LDAP for authenticating users.
192
192
193 .. _ldap_host:
193 .. _ldap_host:
194
194
195 Host : required
195 Host : required
196 LDAP server hostname or IP address.
196 LDAP server hostname or IP address.
197
197
198 .. _Port:
198 .. _Port:
199
199
200 Port : required
200 Port : required
201 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
201 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
202
202
203 .. _ldap_account:
203 .. _ldap_account:
204
204
205 Account : optional
205 Account : optional
206 Only required if the LDAP server does not allow anonymous browsing of
206 Only required if the LDAP server does not allow anonymous browsing of
207 records. This should be a special account for record browsing. This
207 records. This should be a special account for record browsing. This
208 will require `LDAP Password`_ below.
208 will require `LDAP Password`_ below.
209
209
210 .. _LDAP Password:
210 .. _LDAP Password:
211
211
212 Password : optional
212 Password : optional
213 Only required if the LDAP server does not allow anonymous browsing of
213 Only required if the LDAP server does not allow anonymous browsing of
214 records.
214 records.
215
215
216 .. _Enable LDAPS:
216 .. _Enable LDAPS:
217
217
218 Connection Security : required
218 Connection Security : required
219 Defines the connection to LDAP server
219 Defines the connection to LDAP server
220
220
221 No encryption
221 No encryption
222 Plain non encrypted connection
222 Plain non encrypted connection
223
223
224 LDAPS connection
224 LDAPS connection
225 Enable ldaps connection. It will likely require `Port`_ to be set to
225 Enable ldaps connection. It will likely require `Port`_ to be set to
226 a different value (standard LDAPS port is 636). When LDAPS is enabled
226 a different value (standard LDAPS port is 636). When LDAPS is enabled
227 then `Certificate Checks`_ is required.
227 then `Certificate Checks`_ is required.
228
228
229 START_TLS on LDAP connection
229 START_TLS on LDAP connection
230 START TLS connection
230 START TLS connection
231
231
232 .. _Certificate Checks:
232 .. _Certificate Checks:
233
233
234 Certificate Checks : optional
234 Certificate Checks : optional
235 How SSL certificates verification is handled - this is only useful when
235 How SSL certificates verification is handled - this is only useful when
236 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
236 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
237 while the other options are susceptible to man-in-the-middle attacks. SSL
237 while the other options are susceptible to man-in-the-middle attacks. SSL
238 certificates can be installed to /etc/openldap/cacerts so that the
238 certificates can be installed to /etc/openldap/cacerts so that the
239 DEMAND or HARD options can be used with self-signed certificates or
239 DEMAND or HARD options can be used with self-signed certificates or
240 certificates that do not have traceable certificates of authority.
240 certificates that do not have traceable certificates of authority.
241
241
242 NEVER
242 NEVER
243 A serve certificate will never be requested or checked.
243 A serve certificate will never be requested or checked.
244
244
245 ALLOW
245 ALLOW
246 A server certificate is requested. Failure to provide a
246 A server certificate is requested. Failure to provide a
247 certificate or providing a bad certificate will not terminate the
247 certificate or providing a bad certificate will not terminate the
248 session.
248 session.
249
249
250 TRY
250 TRY
251 A server certificate is requested. Failure to provide a
251 A server certificate is requested. Failure to provide a
252 certificate does not halt the session; providing a bad certificate
252 certificate does not halt the session; providing a bad certificate
253 halts the session.
253 halts the session.
254
254
255 DEMAND
255 DEMAND
256 A server certificate is requested and must be provided and
256 A server certificate is requested and must be provided and
257 authenticated for the session to proceed.
257 authenticated for the session to proceed.
258
258
259 HARD
259 HARD
260 The same as DEMAND.
260 The same as DEMAND.
261
261
262 .. _Base DN:
262 .. _Base DN:
263
263
264 Base DN : required
264 Base DN : required
265 The Distinguished Name (DN) where searches for users will be performed.
265 The Distinguished Name (DN) where searches for users will be performed.
266 Searches can be controlled by `LDAP Filter`_ and `LDAP Search Scope`_.
266 Searches can be controlled by `LDAP Filter`_ and `LDAP Search Scope`_.
267
267
268 .. _LDAP Filter:
268 .. _LDAP Filter:
269
269
270 LDAP Filter : optional
270 LDAP Filter : optional
271 A LDAP filter defined by RFC 2254. This is more useful when `LDAP
271 A LDAP filter defined by RFC 2254. This is more useful when `LDAP
272 Search Scope`_ is set to SUBTREE. The filter is useful for limiting
272 Search Scope`_ is set to SUBTREE. The filter is useful for limiting
273 which LDAP objects are identified as representing Users for
273 which LDAP objects are identified as representing Users for
274 authentication. The filter is augmented by `Login Attribute`_ below.
274 authentication. The filter is augmented by `Login Attribute`_ below.
275 This can commonly be left blank.
275 This can commonly be left blank.
276
276
277 .. _LDAP Search Scope:
277 .. _LDAP Search Scope:
278
278
279 LDAP Search Scope : required
279 LDAP Search Scope : required
280 This limits how far LDAP will search for a matching object.
280 This limits how far LDAP will search for a matching object.
281
281
282 BASE
282 BASE
283 Only allows searching of `Base DN`_ and is usually not what you
283 Only allows searching of `Base DN`_ and is usually not what you
284 want.
284 want.
285
285
286 ONELEVEL
286 ONELEVEL
287 Searches all entries under `Base DN`_, but not Base DN itself.
287 Searches all entries under `Base DN`_, but not Base DN itself.
288
288
289 SUBTREE
289 SUBTREE
290 Searches all entries below `Base DN`_, but not Base DN itself.
290 Searches all entries below `Base DN`_, but not Base DN itself.
291 When using SUBTREE `LDAP Filter`_ is useful to limit object
291 When using SUBTREE `LDAP Filter`_ is useful to limit object
292 location.
292 location.
293
293
294 .. _Login Attribute:
294 .. _Login Attribute:
295
295
296 Login Attribute : required
296 Login Attribute : required
297 The LDAP record attribute that will be matched as the USERNAME or
297 The LDAP record attribute that will be matched as the USERNAME or
298 ACCOUNT used to connect to RhodeCode. This will be added to `LDAP
298 ACCOUNT used to connect to RhodeCode. This will be added to `LDAP
299 Filter`_ for locating the User object. If `LDAP Filter`_ is specified as
299 Filter`_ for locating the User object. If `LDAP Filter`_ is specified as
300 "LDAPFILTER", `Login Attribute`_ is specified as "uid" and the user has
300 "LDAPFILTER", `Login Attribute`_ is specified as "uid" and the user has
301 connected as "jsmith" then the `LDAP Filter`_ will be augmented as below
301 connected as "jsmith" then the `LDAP Filter`_ will be augmented as below
302 ::
302 ::
303
303
304 (&(LDAPFILTER)(uid=jsmith))
304 (&(LDAPFILTER)(uid=jsmith))
305
305
306 .. _ldap_attr_firstname:
306 .. _ldap_attr_firstname:
307
307
308 First Name Attribute : required
308 First Name Attribute : required
309 The LDAP record attribute which represents the user's first name.
309 The LDAP record attribute which represents the user's first name.
310
310
311 .. _ldap_attr_lastname:
311 .. _ldap_attr_lastname:
312
312
313 Last Name Attribute : required
313 Last Name Attribute : required
314 The LDAP record attribute which represents the user's last name.
314 The LDAP record attribute which represents the user's last name.
315
315
316 .. _ldap_attr_email:
316 .. _ldap_attr_email:
317
317
318 Email Attribute : required
318 Email Attribute : required
319 The LDAP record attribute which represents the user's email address.
319 The LDAP record attribute which represents the user's email address.
320
320
321 If all data are entered correctly, and python-ldap_ is properly installed
321 If all data are entered correctly, and python-ldap_ is properly installed
322 users should be granted access to RhodeCode with ldap accounts. At this
322 users should be granted access to RhodeCode with ldap accounts. At this
323 time user information is copied from LDAP into the RhodeCode user database.
323 time user information is copied from LDAP into the RhodeCode user database.
324 This means that updates of an LDAP user object may not be reflected as a
324 This means that updates of an LDAP user object may not be reflected as a
325 user update in RhodeCode.
325 user update in RhodeCode.
326
326
327 If You have problems with LDAP access and believe You entered correct
327 If You have problems with LDAP access and believe You entered correct
328 information check out the RhodeCode logs, any error messages sent from LDAP
328 information check out the RhodeCode logs, any error messages sent from LDAP
329 will be saved there.
329 will be saved there.
330
330
331 Active Directory
331 Active Directory
332 ''''''''''''''''
332 ''''''''''''''''
333
333
334 RhodeCode can use Microsoft Active Directory for user authentication. This
334 RhodeCode can use Microsoft Active Directory for user authentication. This
335 is done through an LDAP or LDAPS connection to Active Directory. The
335 is done through an LDAP or LDAPS connection to Active Directory. The
336 following LDAP configuration settings are typical for using Active
336 following LDAP configuration settings are typical for using Active
337 Directory ::
337 Directory ::
338
338
339 Base DN = OU=SBSUsers,OU=Users,OU=MyBusiness,DC=v3sys,DC=local
339 Base DN = OU=SBSUsers,OU=Users,OU=MyBusiness,DC=v3sys,DC=local
340 Login Attribute = sAMAccountName
340 Login Attribute = sAMAccountName
341 First Name Attribute = givenName
341 First Name Attribute = givenName
342 Last Name Attribute = sn
342 Last Name Attribute = sn
343 E-mail Attribute = mail
343 E-mail Attribute = mail
344
344
345 All other LDAP settings will likely be site-specific and should be
345 All other LDAP settings will likely be site-specific and should be
346 appropriately configured.
346 appropriately configured.
347
347
348
348
349
349
350 Authentication by container or reverse-proxy
350 Authentication by container or reverse-proxy
351 --------------------------------------------
351 --------------------------------------------
352
352
353 Starting with version 1.3, RhodeCode supports delegating the authentication
353 Starting with version 1.3, RhodeCode supports delegating the authentication
354 of users to its WSGI container, or to a reverse-proxy server through which all
354 of users to its WSGI container, or to a reverse-proxy server through which all
355 clients access the application.
355 clients access the application.
356
356
357 When these authentication methods are enabled in RhodeCode, it uses the
357 When these authentication methods are enabled in RhodeCode, it uses the
358 username that the container/proxy (Apache/Nginx/etc) authenticated and doesn't
358 username that the container/proxy (Apache/Nginx/etc) authenticated and doesn't
359 perform the authentication itself. The authorization, however, is still done by
359 perform the authentication itself. The authorization, however, is still done by
360 RhodeCode according to its settings.
360 RhodeCode according to its settings.
361
361
362 When a user logs in for the first time using these authentication methods,
362 When a user logs in for the first time using these authentication methods,
363 a matching user account is created in RhodeCode with default permissions. An
363 a matching user account is created in RhodeCode with default permissions. An
364 administrator can then modify it using RhodeCode's admin interface.
364 administrator can then modify it using RhodeCode's admin interface.
365 It's also possible for an administrator to create accounts and configure their
365 It's also possible for an administrator to create accounts and configure their
366 permissions before the user logs in for the first time.
366 permissions before the user logs in for the first time.
367
367
368 Container-based authentication
368 Container-based authentication
369 ''''''''''''''''''''''''''''''
369 ''''''''''''''''''''''''''''''
370
370
371 In a container-based authentication setup, RhodeCode reads the user name from
371 In a container-based authentication setup, RhodeCode reads the user name from
372 the ``REMOTE_USER`` server variable provided by the WSGI container.
372 the ``REMOTE_USER`` server variable provided by the WSGI container.
373
373
374 After setting up your container (see `Apache's WSGI config`_), you'd need
374 After setting up your container (see `Apache's WSGI config`_), you'd need
375 to configure it to require authentication on the location configured for
375 to configure it to require authentication on the location configured for
376 RhodeCode.
376 RhodeCode.
377
377
378 In order for RhodeCode to start using the provided username, you should set the
378 In order for RhodeCode to start using the provided username, you should set the
379 following in the [app:main] section of your .ini file::
379 following in the [app:main] section of your .ini file::
380
380
381 container_auth_enabled = true
381 container_auth_enabled = true
382
382
383
383
384 Proxy pass-through authentication
384 Proxy pass-through authentication
385 '''''''''''''''''''''''''''''''''
385 '''''''''''''''''''''''''''''''''
386
386
387 In a proxy pass-through authentication setup, RhodeCode reads the user name
387 In a proxy pass-through authentication setup, RhodeCode reads the user name
388 from the ``X-Forwarded-User`` request header, which should be configured to be
388 from the ``X-Forwarded-User`` request header, which should be configured to be
389 sent by the reverse-proxy server.
389 sent by the reverse-proxy server.
390
390
391 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
391 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
392 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'd need to
392 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'd need to
393 configure the authentication and add the username in a request header named
393 configure the authentication and add the username in a request header named
394 ``X-Forwarded-User``.
394 ``X-Forwarded-User``.
395
395
396 For example, the following config section for Apache sets a subdirectory in a
396 For example, the following config section for Apache sets a subdirectory in a
397 reverse-proxy setup with basic auth::
397 reverse-proxy setup with basic auth::
398
398
399 <Location /<someprefix> >
399 <Location /<someprefix> >
400 ProxyPass http://127.0.0.1:5000/<someprefix>
400 ProxyPass http://127.0.0.1:5000/<someprefix>
401 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
401 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
402 SetEnvIf X-Url-Scheme https HTTPS=1
402 SetEnvIf X-Url-Scheme https HTTPS=1
403
403
404 AuthType Basic
404 AuthType Basic
405 AuthName "RhodeCode authentication"
405 AuthName "RhodeCode authentication"
406 AuthUserFile /home/web/rhodecode/.htpasswd
406 AuthUserFile /home/web/rhodecode/.htpasswd
407 require valid-user
407 require valid-user
408
408
409 RequestHeader unset X-Forwarded-User
409 RequestHeader unset X-Forwarded-User
410
410
411 RewriteEngine On
411 RewriteEngine On
412 RewriteCond %{LA-U:REMOTE_USER} (.+)
412 RewriteCond %{LA-U:REMOTE_USER} (.+)
413 RewriteRule .* - [E=RU:%1]
413 RewriteRule .* - [E=RU:%1]
414 RequestHeader set X-Forwarded-User %{RU}e
414 RequestHeader set X-Forwarded-User %{RU}e
415 </Location>
415 </Location>
416
416
417 In order for RhodeCode to start using the forwarded username, you should set
417 In order for RhodeCode to start using the forwarded username, you should set
418 the following in the [app:main] section of your .ini file::
418 the following in the [app:main] section of your .ini file::
419
419
420 proxypass_auth_enabled = true
420 proxypass_auth_enabled = true
421
421
422 .. note::
422 .. note::
423 If you enable proxy pass-through authentication, make sure your server is
423 If you enable proxy pass-through authentication, make sure your server is
424 only accessible through the proxy. Otherwise, any client would be able to
424 only accessible through the proxy. Otherwise, any client would be able to
425 forge the authentication header and could effectively become authenticated
425 forge the authentication header and could effectively become authenticated
426 using any account of their liking.
426 using any account of their liking.
427
427
428 Integration with Issue trackers
429 -------------------------------
428
430
431 RhodeCode provides a simple integration with issue trackers. It's possible
432 to define a regular expression that will fetch issue id stored in commit
433 messages and replace that with an url to this issue. To enable this simply
434 uncomment following variables in the ini file::
435
436 url_pat = (?:^#|\s#)(\w+)
437 issue_server = https://myissueserver.com/issue/{id}
438 issue_prefix = #
439
440 `url_pat` is the regular expression that will match issues, default given regex
441 will match issues in format of #<number> eg. #300.
442 Matched issues will be replace with the `issue_server` url replacing {id} with
443 id fetched from regex. Since the # is striped `issue_prefix` is added as a
444 prefix to url. `issue_prefix` can be something different than # if you pass
445 ISSUE- as issue prefix this will generate an url in format
446 `<a href="https://myissueserver.com/issue/300">ISSUE-300</a>`
429
447
430 Hook management
448 Hook management
431 ---------------
449 ---------------
432
450
433 Hooks can be managed in similar way to this used in .hgrc files.
451 Hooks can be managed in similar way to this used in .hgrc files.
434 To access hooks setting click `advanced setup` on Hooks section of Mercurial
452 To access hooks setting click `advanced setup` on Hooks section of Mercurial
435 Settings in Admin.
453 Settings in Admin.
436
454
437 There are 4 built in hooks that cannot be changed (only enable/disable by
455 There are 4 built in hooks that cannot be changed (only enable/disable by
438 checkboxes on previos section).
456 checkboxes on previos section).
439 To add another custom hook simply fill in first section with
457 To add another custom hook simply fill in first section with
440 <name>.<hook_type> and the second one with hook path. Example hooks
458 <name>.<hook_type> and the second one with hook path. Example hooks
441 can be found at *rhodecode.lib.hooks*.
459 can be found at *rhodecode.lib.hooks*.
442
460
443
461
444 Setting Up Celery
462 Setting Up Celery
445 -----------------
463 -----------------
446
464
447 Since version 1.1 celery is configured by the rhodecode ini configuration files.
465 Since version 1.1 celery is configured by the rhodecode ini configuration files.
448 Simply set use_celery=true in the ini file then add / change the configuration
466 Simply set use_celery=true in the ini file then add / change the configuration
449 variables inside the ini file.
467 variables inside the ini file.
450
468
451 Remember that the ini files use the format with '.' not with '_' like celery.
469 Remember that the ini files use the format with '.' not with '_' like celery.
452 So for example setting `BROKER_HOST` in celery means setting `broker.host` in
470 So for example setting `BROKER_HOST` in celery means setting `broker.host` in
453 the config file.
471 the config file.
454
472
455 In order to start using celery run::
473 In order to start using celery run::
456
474
457 paster celeryd <configfile.ini>
475 paster celeryd <configfile.ini>
458
476
459
477
460 .. note::
478 .. note::
461 Make sure you run this command from the same virtualenv, and with the same
479 Make sure you run this command from the same virtualenv, and with the same
462 user that rhodecode runs.
480 user that rhodecode runs.
463
481
464 HTTPS support
482 HTTPS support
465 -------------
483 -------------
466
484
467 There are two ways to enable https:
485 There are two ways to enable https:
468
486
469 - Set HTTP_X_URL_SCHEME in your http server headers, than rhodecode will
487 - Set HTTP_X_URL_SCHEME in your http server headers, than rhodecode will
470 recognize this headers and make proper https redirections
488 recognize this headers and make proper https redirections
471 - Alternatively, change the `force_https = true` flag in the ini configuration
489 - Alternatively, change the `force_https = true` flag in the ini configuration
472 to force using https, no headers are needed than to enable https
490 to force using https, no headers are needed than to enable https
473
491
474
492
475 Nginx virtual host example
493 Nginx virtual host example
476 --------------------------
494 --------------------------
477
495
478 Sample config for nginx using proxy::
496 Sample config for nginx using proxy::
479
497
480 upstream rc {
498 upstream rc {
481 server 127.0.0.1:5000;
499 server 127.0.0.1:5000;
482 # add more instances for load balancing
500 # add more instances for load balancing
483 #server 127.0.0.1:5001;
501 #server 127.0.0.1:5001;
484 #server 127.0.0.1:5002;
502 #server 127.0.0.1:5002;
485 }
503 }
486
504
487 server {
505 server {
488 listen 80;
506 listen 80;
489 server_name hg.myserver.com;
507 server_name hg.myserver.com;
490 access_log /var/log/nginx/rhodecode.access.log;
508 access_log /var/log/nginx/rhodecode.access.log;
491 error_log /var/log/nginx/rhodecode.error.log;
509 error_log /var/log/nginx/rhodecode.error.log;
492
510
493 location / {
511 location / {
494 try_files $uri @rhode;
512 try_files $uri @rhode;
495 }
513 }
496
514
497 location @rhode {
515 location @rhode {
498 proxy_pass http://rc;
516 proxy_pass http://rc;
499 include /etc/nginx/proxy.conf;
517 include /etc/nginx/proxy.conf;
500 }
518 }
501
519
502 }
520 }
503
521
504 Here's the proxy.conf. It's tuned so it will not timeout on long
522 Here's the proxy.conf. It's tuned so it will not timeout on long
505 pushes or large pushes::
523 pushes or large pushes::
506
524
507 proxy_redirect off;
525 proxy_redirect off;
508 proxy_set_header Host $host;
526 proxy_set_header Host $host;
509 proxy_set_header X-Url-Scheme $scheme;
527 proxy_set_header X-Url-Scheme $scheme;
510 proxy_set_header X-Host $http_host;
528 proxy_set_header X-Host $http_host;
511 proxy_set_header X-Real-IP $remote_addr;
529 proxy_set_header X-Real-IP $remote_addr;
512 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
530 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
513 proxy_set_header Proxy-host $proxy_host;
531 proxy_set_header Proxy-host $proxy_host;
514 client_max_body_size 400m;
532 client_max_body_size 400m;
515 client_body_buffer_size 128k;
533 client_body_buffer_size 128k;
516 proxy_buffering off;
534 proxy_buffering off;
517 proxy_connect_timeout 7200;
535 proxy_connect_timeout 7200;
518 proxy_send_timeout 7200;
536 proxy_send_timeout 7200;
519 proxy_read_timeout 7200;
537 proxy_read_timeout 7200;
520 proxy_buffers 8 32k;
538 proxy_buffers 8 32k;
521
539
522 Also, when using root path with nginx you might set the static files to false
540 Also, when using root path with nginx you might set the static files to false
523 in the production.ini file::
541 in the production.ini file::
524
542
525 [app:main]
543 [app:main]
526 use = egg:rhodecode
544 use = egg:rhodecode
527 full_stack = true
545 full_stack = true
528 static_files = false
546 static_files = false
529 lang=en
547 lang=en
530 cache_dir = %(here)s/data
548 cache_dir = %(here)s/data
531
549
532 In order to not have the statics served by the application. This improves speed.
550 In order to not have the statics served by the application. This improves speed.
533
551
534
552
535 Apache virtual host reverse proxy example
553 Apache virtual host reverse proxy example
536 -----------------------------------------
554 -----------------------------------------
537
555
538 Here is a sample configuration file for apache using proxy::
556 Here is a sample configuration file for apache using proxy::
539
557
540 <VirtualHost *:80>
558 <VirtualHost *:80>
541 ServerName hg.myserver.com
559 ServerName hg.myserver.com
542 ServerAlias hg.myserver.com
560 ServerAlias hg.myserver.com
543
561
544 <Proxy *>
562 <Proxy *>
545 Order allow,deny
563 Order allow,deny
546 Allow from all
564 Allow from all
547 </Proxy>
565 </Proxy>
548
566
549 #important !
567 #important !
550 #Directive to properly generate url (clone url) for pylons
568 #Directive to properly generate url (clone url) for pylons
551 ProxyPreserveHost On
569 ProxyPreserveHost On
552
570
553 #rhodecode instance
571 #rhodecode instance
554 ProxyPass / http://127.0.0.1:5000/
572 ProxyPass / http://127.0.0.1:5000/
555 ProxyPassReverse / http://127.0.0.1:5000/
573 ProxyPassReverse / http://127.0.0.1:5000/
556
574
557 #to enable https use line below
575 #to enable https use line below
558 #SetEnvIf X-Url-Scheme https HTTPS=1
576 #SetEnvIf X-Url-Scheme https HTTPS=1
559
577
560 </VirtualHost>
578 </VirtualHost>
561
579
562
580
563 Additional tutorial
581 Additional tutorial
564 http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons
582 http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons
565
583
566
584
567 Apache as subdirectory
585 Apache as subdirectory
568 ----------------------
586 ----------------------
569
587
570 Apache subdirectory part::
588 Apache subdirectory part::
571
589
572 <Location /<someprefix> >
590 <Location /<someprefix> >
573 ProxyPass http://127.0.0.1:5000/<someprefix>
591 ProxyPass http://127.0.0.1:5000/<someprefix>
574 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
592 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
575 SetEnvIf X-Url-Scheme https HTTPS=1
593 SetEnvIf X-Url-Scheme https HTTPS=1
576 </Location>
594 </Location>
577
595
578 Besides the regular apache setup you will need to add the following line
596 Besides the regular apache setup you will need to add the following line
579 into [app:main] section of your .ini file::
597 into [app:main] section of your .ini file::
580
598
581 filter-with = proxy-prefix
599 filter-with = proxy-prefix
582
600
583 Add the following at the end of the .ini file::
601 Add the following at the end of the .ini file::
584
602
585 [filter:proxy-prefix]
603 [filter:proxy-prefix]
586 use = egg:PasteDeploy#prefix
604 use = egg:PasteDeploy#prefix
587 prefix = /<someprefix>
605 prefix = /<someprefix>
588
606
589
607
590 then change <someprefix> into your choosen prefix
608 then change <someprefix> into your choosen prefix
591
609
592 Apache's WSGI config
610 Apache's WSGI config
593 --------------------
611 --------------------
594
612
595 Alternatively, RhodeCode can be set up with Apache under mod_wsgi. For
613 Alternatively, RhodeCode can be set up with Apache under mod_wsgi. For
596 that, you'll need to:
614 that, you'll need to:
597
615
598 - Install mod_wsgi. If using a Debian-based distro, you can install
616 - Install mod_wsgi. If using a Debian-based distro, you can install
599 the package libapache2-mod-wsgi::
617 the package libapache2-mod-wsgi::
600
618
601 aptitude install libapache2-mod-wsgi
619 aptitude install libapache2-mod-wsgi
602
620
603 - Enable mod_wsgi::
621 - Enable mod_wsgi::
604
622
605 a2enmod wsgi
623 a2enmod wsgi
606
624
607 - Create a wsgi dispatch script, like the one below. Make sure you
625 - Create a wsgi dispatch script, like the one below. Make sure you
608 check the paths correctly point to where you installed RhodeCode
626 check the paths correctly point to where you installed RhodeCode
609 and its Python Virtual Environment.
627 and its Python Virtual Environment.
610 - Enable the WSGIScriptAlias directive for the wsgi dispatch script,
628 - Enable the WSGIScriptAlias directive for the wsgi dispatch script,
611 as in the following example. Once again, check the paths are
629 as in the following example. Once again, check the paths are
612 correctly specified.
630 correctly specified.
613
631
614 Here is a sample excerpt from an Apache Virtual Host configuration file::
632 Here is a sample excerpt from an Apache Virtual Host configuration file::
615
633
616 WSGIDaemonProcess pylons user=www-data group=www-data processes=1 \
634 WSGIDaemonProcess pylons user=www-data group=www-data processes=1 \
617 threads=4 \
635 threads=4 \
618 python-path=/home/web/rhodecode/pyenv/lib/python2.6/site-packages
636 python-path=/home/web/rhodecode/pyenv/lib/python2.6/site-packages
619 WSGIScriptAlias / /home/web/rhodecode/dispatch.wsgi
637 WSGIScriptAlias / /home/web/rhodecode/dispatch.wsgi
620
638
621 Example wsgi dispatch script::
639 Example wsgi dispatch script::
622
640
623 import os
641 import os
624 os.environ["HGENCODING"] = "UTF-8"
642 os.environ["HGENCODING"] = "UTF-8"
625 os.environ['PYTHON_EGG_CACHE'] = '/home/web/rhodecode/.egg-cache'
643 os.environ['PYTHON_EGG_CACHE'] = '/home/web/rhodecode/.egg-cache'
626
644
627 # sometimes it's needed to set the curent dir
645 # sometimes it's needed to set the curent dir
628 os.chdir('/home/web/rhodecode/')
646 os.chdir('/home/web/rhodecode/')
629
647
630 import site
648 import site
631 site.addsitedir("/home/web/rhodecode/pyenv/lib/python2.6/site-packages")
649 site.addsitedir("/home/web/rhodecode/pyenv/lib/python2.6/site-packages")
632
650
633 from paste.deploy import loadapp
651 from paste.deploy import loadapp
634 from paste.script.util.logging_config import fileConfig
652 from paste.script.util.logging_config import fileConfig
635
653
636 fileConfig('/home/web/rhodecode/production.ini')
654 fileConfig('/home/web/rhodecode/production.ini')
637 application = loadapp('config:/home/web/rhodecode/production.ini')
655 application = loadapp('config:/home/web/rhodecode/production.ini')
638
656
639 Note: when using mod_wsgi you'll need to install the same version of
657 Note: when using mod_wsgi you'll need to install the same version of
640 Mercurial that's inside RhodeCode's virtualenv also on the system's Python
658 Mercurial that's inside RhodeCode's virtualenv also on the system's Python
641 environment.
659 environment.
642
660
643
661
644 Other configuration files
662 Other configuration files
645 -------------------------
663 -------------------------
646
664
647 Some example init.d scripts can be found here, for debian and gentoo:
665 Some example init.d scripts can be found here, for debian and gentoo:
648
666
649 https://rhodecode.org/rhodecode/files/tip/init.d
667 https://rhodecode.org/rhodecode/files/tip/init.d
650
668
651
669
652 Troubleshooting
670 Troubleshooting
653 ---------------
671 ---------------
654
672
655 :Q: **Missing static files?**
673 :Q: **Missing static files?**
656 :A: Make sure either to set the `static_files = true` in the .ini file or
674 :A: Make sure either to set the `static_files = true` in the .ini file or
657 double check the root path for your http setup. It should point to
675 double check the root path for your http setup. It should point to
658 for example:
676 for example:
659 /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
677 /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
660
678
661 |
679 |
662
680
663 :Q: **Can't install celery/rabbitmq**
681 :Q: **Can't install celery/rabbitmq**
664 :A: Don't worry RhodeCode works without them too. No extra setup is required.
682 :A: Don't worry RhodeCode works without them too. No extra setup is required.
665
683
666 |
684 |
667
685
668 :Q: **Long lasting push timeouts?**
686 :Q: **Long lasting push timeouts?**
669 :A: Make sure you set a longer timeouts in your proxy/fcgi settings, timeouts
687 :A: Make sure you set a longer timeouts in your proxy/fcgi settings, timeouts
670 are caused by https server and not RhodeCode.
688 are caused by https server and not RhodeCode.
671
689
672 |
690 |
673
691
674 :Q: **Large pushes timeouts?**
692 :Q: **Large pushes timeouts?**
675 :A: Make sure you set a proper max_body_size for the http server.
693 :A: Make sure you set a proper max_body_size for the http server.
676
694
677 |
695 |
678
696
679 :Q: **Apache doesn't pass basicAuth on pull/push?**
697 :Q: **Apache doesn't pass basicAuth on pull/push?**
680 :A: Make sure you added `WSGIPassAuthorization true`.
698 :A: Make sure you added `WSGIPassAuthorization true`.
681
699
682 For further questions search the `Issues tracker`_, or post a message in the
700 For further questions search the `Issues tracker`_, or post a message in the
683 `google group rhodecode`_
701 `google group rhodecode`_
684
702
685 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
703 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
686 .. _python: http://www.python.org/
704 .. _python: http://www.python.org/
687 .. _mercurial: http://mercurial.selenic.com/
705 .. _mercurial: http://mercurial.selenic.com/
688 .. _celery: http://celeryproject.org/
706 .. _celery: http://celeryproject.org/
689 .. _rabbitmq: http://www.rabbitmq.com/
707 .. _rabbitmq: http://www.rabbitmq.com/
690 .. _python-ldap: http://www.python-ldap.org/
708 .. _python-ldap: http://www.python-ldap.org/
691 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
709 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
692 .. _PublishingRepositories: http://mercurial.selenic.com/wiki/PublishingRepositories
710 .. _PublishingRepositories: http://mercurial.selenic.com/wiki/PublishingRepositories
693 .. _Issues tracker: https://bitbucket.org/marcinkuzminski/rhodecode/issues
711 .. _Issues tracker: https://bitbucket.org/marcinkuzminski/rhodecode/issues
694 .. _google group rhodecode: http://groups.google.com/group/rhodecode
712 .. _google group rhodecode: http://groups.google.com/group/rhodecode
@@ -1,267 +1,288 b''
1 ################################################################################
1 ################################################################################
2 ################################################################################
2 ################################################################################
3 # RhodeCode - Pylons environment configuration #
3 # RhodeCode - Pylons environment configuration #
4 # #
4 # #
5 # The %(here)s variable will be replaced with the parent directory of this file#
5 # The %(here)s variable will be replaced with the parent directory of this file#
6 ################################################################################
6 ################################################################################
7
7
8 [DEFAULT]
8 [DEFAULT]
9 debug = true
9 debug = true
10 pdebug = false
10 pdebug = false
11 ################################################################################
11 ################################################################################
12 ## Uncomment and replace with the address which should receive ##
12 ## Uncomment and replace with the address which should receive ##
13 ## any error reports after application crash ##
13 ## any error reports after application crash ##
14 ## Additionally those settings will be used by RhodeCode mailing system ##
14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 ################################################################################
15 ################################################################################
16 #email_to = admin@localhost
16 #email_to = admin@localhost
17 #error_email_from = paste_error@localhost
17 #error_email_from = paste_error@localhost
18 #app_email_from = rhodecode-noreply@localhost
18 #app_email_from = rhodecode-noreply@localhost
19 #error_message =
19 #error_message =
20 #email_prefix = [RhodeCode]
20 #email_prefix = [RhodeCode]
21
21
22 #smtp_server = mail.server.com
22 #smtp_server = mail.server.com
23 #smtp_username =
23 #smtp_username =
24 #smtp_password =
24 #smtp_password =
25 #smtp_port =
25 #smtp_port =
26 #smtp_use_tls = false
26 #smtp_use_tls = false
27 #smtp_use_ssl = true
27 #smtp_use_ssl = true
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 #smtp_auth =
29 #smtp_auth =
30
30
31 [server:main]
31 [server:main]
32 ##nr of threads to spawn
32 ##nr of threads to spawn
33 threadpool_workers = 5
33 threadpool_workers = 5
34
34
35 ##max request before thread respawn
35 ##max request before thread respawn
36 threadpool_max_requests = 10
36 threadpool_max_requests = 10
37
37
38 ##option to use threads of process
38 ##option to use threads of process
39 use_threadpool = true
39 use_threadpool = true
40
40
41 use = egg:Paste#http
41 use = egg:Paste#http
42 host = 127.0.0.1
42 host = 127.0.0.1
43 port = 8001
43 port = 8001
44
44
45 [app:main]
45 [app:main]
46 use = egg:rhodecode
46 use = egg:rhodecode
47 full_stack = true
47 full_stack = true
48 static_files = true
48 static_files = true
49 lang=en
49 lang=en
50 cache_dir = %(here)s/data
50 cache_dir = %(here)s/data
51 index_dir = %(here)s/data/index
51 index_dir = %(here)s/data/index
52 app_instance_uuid = prod1234
52 app_instance_uuid = prod1234
53 cut_off_limit = 256000
53 cut_off_limit = 256000
54 force_https = false
54 force_https = false
55 commit_parse_limit = 50
55 commit_parse_limit = 50
56 use_gravatar = true
56 use_gravatar = true
57 container_auth_enabled = false
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
58 proxypass_auth_enabled = false
59
59
60 ## overwrite schema of clone url
60 ## overwrite schema of clone url
61 # available vars:
61 ## available vars:
62 # scheme - http/https
62 ## scheme - http/https
63 # user - current user
63 ## user - current user
64 # pass - password
64 ## pass - password
65 # netloc - network location
65 ## netloc - network location
66 # path - usually repo_name
66 ## path - usually repo_name
67 # clone_uri = {scheme}://{user}{pass}{netloc}{path}
67
68 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
69
70 ## issue tracking mapping for commits messages
71 ## uncomment url_pat, issue_server, issue_prefix to enable
72
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #1234
76
77 #url_pat = (?:^#|\s#)(\w+)
78
79 ## server url to the issue, each {id} will be replaced with id
80 ## fetched from the regex
81
82 #issue_server = https://myissueserver.com/issue/{id}
83
84 ## prefix to add to link to indicate it's an url
85 ## #314 will be replaced by <issue_prefix><id>
86
87 #issue_prefix = #
88
68
89
69 ####################################
90 ####################################
70 ### CELERY CONFIG ####
91 ### CELERY CONFIG ####
71 ####################################
92 ####################################
72 use_celery = false
93 use_celery = false
73 broker.host = localhost
94 broker.host = localhost
74 broker.vhost = rabbitmqhost
95 broker.vhost = rabbitmqhost
75 broker.port = 5672
96 broker.port = 5672
76 broker.user = rabbitmq
97 broker.user = rabbitmq
77 broker.password = qweqwe
98 broker.password = qweqwe
78
99
79 celery.imports = rhodecode.lib.celerylib.tasks
100 celery.imports = rhodecode.lib.celerylib.tasks
80
101
81 celery.result.backend = amqp
102 celery.result.backend = amqp
82 celery.result.dburi = amqp://
103 celery.result.dburi = amqp://
83 celery.result.serialier = json
104 celery.result.serialier = json
84
105
85 #celery.send.task.error.emails = true
106 #celery.send.task.error.emails = true
86 #celery.amqp.task.result.expires = 18000
107 #celery.amqp.task.result.expires = 18000
87
108
88 celeryd.concurrency = 2
109 celeryd.concurrency = 2
89 #celeryd.log.file = celeryd.log
110 #celeryd.log.file = celeryd.log
90 celeryd.log.level = debug
111 celeryd.log.level = debug
91 celeryd.max.tasks.per.child = 1
112 celeryd.max.tasks.per.child = 1
92
113
93 #tasks will never be sent to the queue, but executed locally instead.
114 #tasks will never be sent to the queue, but executed locally instead.
94 celery.always.eager = false
115 celery.always.eager = false
95
116
96 ####################################
117 ####################################
97 ### BEAKER CACHE ####
118 ### BEAKER CACHE ####
98 ####################################
119 ####################################
99 beaker.cache.data_dir=%(here)s/data/cache/data
120 beaker.cache.data_dir=%(here)s/data/cache/data
100 beaker.cache.lock_dir=%(here)s/data/cache/lock
121 beaker.cache.lock_dir=%(here)s/data/cache/lock
101
122
102 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
123 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
103
124
104 beaker.cache.super_short_term.type=memory
125 beaker.cache.super_short_term.type=memory
105 beaker.cache.super_short_term.expire=10
126 beaker.cache.super_short_term.expire=10
106 beaker.cache.super_short_term.key_length = 256
127 beaker.cache.super_short_term.key_length = 256
107
128
108 beaker.cache.short_term.type=memory
129 beaker.cache.short_term.type=memory
109 beaker.cache.short_term.expire=60
130 beaker.cache.short_term.expire=60
110 beaker.cache.short_term.key_length = 256
131 beaker.cache.short_term.key_length = 256
111
132
112 beaker.cache.long_term.type=memory
133 beaker.cache.long_term.type=memory
113 beaker.cache.long_term.expire=36000
134 beaker.cache.long_term.expire=36000
114 beaker.cache.long_term.key_length = 256
135 beaker.cache.long_term.key_length = 256
115
136
116 beaker.cache.sql_cache_short.type=memory
137 beaker.cache.sql_cache_short.type=memory
117 beaker.cache.sql_cache_short.expire=10
138 beaker.cache.sql_cache_short.expire=10
118 beaker.cache.sql_cache_short.key_length = 256
139 beaker.cache.sql_cache_short.key_length = 256
119
140
120 beaker.cache.sql_cache_med.type=memory
141 beaker.cache.sql_cache_med.type=memory
121 beaker.cache.sql_cache_med.expire=360
142 beaker.cache.sql_cache_med.expire=360
122 beaker.cache.sql_cache_med.key_length = 256
143 beaker.cache.sql_cache_med.key_length = 256
123
144
124 beaker.cache.sql_cache_long.type=file
145 beaker.cache.sql_cache_long.type=file
125 beaker.cache.sql_cache_long.expire=3600
146 beaker.cache.sql_cache_long.expire=3600
126 beaker.cache.sql_cache_long.key_length = 256
147 beaker.cache.sql_cache_long.key_length = 256
127
148
128 ####################################
149 ####################################
129 ### BEAKER SESSION ####
150 ### BEAKER SESSION ####
130 ####################################
151 ####################################
131 ## Type of storage used for the session, current types are
152 ## Type of storage used for the session, current types are
132 ## dbm, file, memcached, database, and memory.
153 ## dbm, file, memcached, database, and memory.
133 ## The storage uses the Container API
154 ## The storage uses the Container API
134 ## that is also used by the cache system.
155 ## that is also used by the cache system.
135
156
136 ## db session example
157 ## db session example
137
158
138 #beaker.session.type = ext:database
159 #beaker.session.type = ext:database
139 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
160 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
140 #beaker.session.table_name = db_session
161 #beaker.session.table_name = db_session
141
162
142 ## encrypted cookie session, good for many instances
163 ## encrypted cookie session, good for many instances
143 #beaker.session.type = cookie
164 #beaker.session.type = cookie
144
165
145 beaker.session.type = file
166 beaker.session.type = file
146 beaker.session.key = rhodecode
167 beaker.session.key = rhodecode
147 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
168 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
148 #beaker.session.validate_key = 9712sds2212c--zxc123
169 #beaker.session.validate_key = 9712sds2212c--zxc123
149 beaker.session.timeout = 36000
170 beaker.session.timeout = 36000
150 beaker.session.httponly = true
171 beaker.session.httponly = true
151
172
152 ## uncomment for https secure cookie
173 ## uncomment for https secure cookie
153 beaker.session.secure = false
174 beaker.session.secure = false
154
175
155 ##auto save the session to not to use .save()
176 ##auto save the session to not to use .save()
156 beaker.session.auto = False
177 beaker.session.auto = False
157
178
158 ##true exire at browser close
179 ##true exire at browser close
159 #beaker.session.cookie_expires = 3600
180 #beaker.session.cookie_expires = 3600
160
181
161
182
162 ################################################################################
183 ################################################################################
163 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
184 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
164 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
185 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
165 ## execute malicious code after an exception is raised. ##
186 ## execute malicious code after an exception is raised. ##
166 ################################################################################
187 ################################################################################
167 set debug = false
188 set debug = false
168
189
169 ##################################
190 ##################################
170 ### LOGVIEW CONFIG ###
191 ### LOGVIEW CONFIG ###
171 ##################################
192 ##################################
172 logview.sqlalchemy = #faa
193 logview.sqlalchemy = #faa
173 logview.pylons.templating = #bfb
194 logview.pylons.templating = #bfb
174 logview.pylons.util = #eee
195 logview.pylons.util = #eee
175
196
176 #########################################################
197 #########################################################
177 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
198 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
178 #########################################################
199 #########################################################
179 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
200 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
180 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
201 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
181 sqlalchemy.db1.echo = false
202 sqlalchemy.db1.echo = false
182 sqlalchemy.db1.pool_recycle = 3600
203 sqlalchemy.db1.pool_recycle = 3600
183 sqlalchemy.convert_unicode = true
204 sqlalchemy.convert_unicode = true
184
205
185 ################################
206 ################################
186 ### LOGGING CONFIGURATION ####
207 ### LOGGING CONFIGURATION ####
187 ################################
208 ################################
188 [loggers]
209 [loggers]
189 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
210 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
190
211
191 [handlers]
212 [handlers]
192 keys = console, console_sql
213 keys = console, console_sql
193
214
194 [formatters]
215 [formatters]
195 keys = generic, color_formatter, color_formatter_sql
216 keys = generic, color_formatter, color_formatter_sql
196
217
197 #############
218 #############
198 ## LOGGERS ##
219 ## LOGGERS ##
199 #############
220 #############
200 [logger_root]
221 [logger_root]
201 level = NOTSET
222 level = NOTSET
202 handlers = console
223 handlers = console
203
224
204 [logger_routes]
225 [logger_routes]
205 level = DEBUG
226 level = DEBUG
206 handlers =
227 handlers =
207 qualname = routes.middleware
228 qualname = routes.middleware
208 # "level = DEBUG" logs the route matched and routing variables.
229 # "level = DEBUG" logs the route matched and routing variables.
209 propagate = 1
230 propagate = 1
210
231
211 [logger_beaker]
232 [logger_beaker]
212 level = DEBUG
233 level = DEBUG
213 handlers =
234 handlers =
214 qualname = beaker.container
235 qualname = beaker.container
215 propagate = 1
236 propagate = 1
216
237
217 [logger_templates]
238 [logger_templates]
218 level = INFO
239 level = INFO
219 handlers =
240 handlers =
220 qualname = pylons.templating
241 qualname = pylons.templating
221 propagate = 1
242 propagate = 1
222
243
223 [logger_rhodecode]
244 [logger_rhodecode]
224 level = DEBUG
245 level = DEBUG
225 handlers =
246 handlers =
226 qualname = rhodecode
247 qualname = rhodecode
227 propagate = 1
248 propagate = 1
228
249
229 [logger_sqlalchemy]
250 [logger_sqlalchemy]
230 level = INFO
251 level = INFO
231 handlers = console_sql
252 handlers = console_sql
232 qualname = sqlalchemy.engine
253 qualname = sqlalchemy.engine
233 propagate = 0
254 propagate = 0
234
255
235 ##############
256 ##############
236 ## HANDLERS ##
257 ## HANDLERS ##
237 ##############
258 ##############
238
259
239 [handler_console]
260 [handler_console]
240 class = StreamHandler
261 class = StreamHandler
241 args = (sys.stderr,)
262 args = (sys.stderr,)
242 level = INFO
263 level = INFO
243 formatter = generic
264 formatter = generic
244
265
245 [handler_console_sql]
266 [handler_console_sql]
246 class = StreamHandler
267 class = StreamHandler
247 args = (sys.stderr,)
268 args = (sys.stderr,)
248 level = WARN
269 level = WARN
249 formatter = generic
270 formatter = generic
250
271
251 ################
272 ################
252 ## FORMATTERS ##
273 ## FORMATTERS ##
253 ################
274 ################
254
275
255 [formatter_generic]
276 [formatter_generic]
256 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
277 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
257 datefmt = %Y-%m-%d %H:%M:%S
278 datefmt = %Y-%m-%d %H:%M:%S
258
279
259 [formatter_color_formatter]
280 [formatter_color_formatter]
260 class=rhodecode.lib.colored_formatter.ColorFormatter
281 class=rhodecode.lib.colored_formatter.ColorFormatter
261 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
282 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
262 datefmt = %Y-%m-%d %H:%M:%S
283 datefmt = %Y-%m-%d %H:%M:%S
263
284
264 [formatter_color_formatter_sql]
285 [formatter_color_formatter_sql]
265 class=rhodecode.lib.colored_formatter.ColorFormatterSql
286 class=rhodecode.lib.colored_formatter.ColorFormatterSql
266 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
287 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
267 datefmt = %Y-%m-%d %H:%M:%S
288 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,277 +1,298 b''
1 ################################################################################
1 ################################################################################
2 ################################################################################
2 ################################################################################
3 # RhodeCode - Pylons environment configuration #
3 # RhodeCode - Pylons environment configuration #
4 # #
4 # #
5 # The %(here)s variable will be replaced with the parent directory of this file#
5 # The %(here)s variable will be replaced with the parent directory of this file#
6 ################################################################################
6 ################################################################################
7
7
8 [DEFAULT]
8 [DEFAULT]
9 debug = true
9 debug = true
10 pdebug = false
10 pdebug = false
11 ################################################################################
11 ################################################################################
12 ## Uncomment and replace with the address which should receive ##
12 ## Uncomment and replace with the address which should receive ##
13 ## any error reports after application crash ##
13 ## any error reports after application crash ##
14 ## Additionally those settings will be used by RhodeCode mailing system ##
14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 ################################################################################
15 ################################################################################
16 #email_to = admin@localhost
16 #email_to = admin@localhost
17 #error_email_from = paste_error@localhost
17 #error_email_from = paste_error@localhost
18 #app_email_from = rhodecode-noreply@localhost
18 #app_email_from = rhodecode-noreply@localhost
19 #error_message =
19 #error_message =
20 #email_prefix = [RhodeCode]
20 #email_prefix = [RhodeCode]
21
21
22 #smtp_server = mail.server.com
22 #smtp_server = mail.server.com
23 #smtp_username =
23 #smtp_username =
24 #smtp_password =
24 #smtp_password =
25 #smtp_port =
25 #smtp_port =
26 #smtp_use_tls = false
26 #smtp_use_tls = false
27 #smtp_use_ssl = true
27 #smtp_use_ssl = true
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 #smtp_auth =
29 #smtp_auth =
30
30
31 [server:main]
31 [server:main]
32 ##nr of threads to spawn
32 ##nr of threads to spawn
33 threadpool_workers = 5
33 threadpool_workers = 5
34
34
35 ##max request before thread respawn
35 ##max request before thread respawn
36 threadpool_max_requests = 10
36 threadpool_max_requests = 10
37
37
38 ##option to use threads of process
38 ##option to use threads of process
39 use_threadpool = true
39 use_threadpool = true
40
40
41 use = egg:Paste#http
41 use = egg:Paste#http
42 host = 127.0.0.1
42 host = 127.0.0.1
43 port = 5000
43 port = 5000
44
44
45 [app:main]
45 [app:main]
46 use = egg:rhodecode
46 use = egg:rhodecode
47 full_stack = true
47 full_stack = true
48 static_files = true
48 static_files = true
49 lang=en
49 lang=en
50 cache_dir = %(here)s/data
50 cache_dir = %(here)s/data
51 index_dir = %(here)s/data/index
51 index_dir = %(here)s/data/index
52 app_instance_uuid = ${app_instance_uuid}
52 app_instance_uuid = ${app_instance_uuid}
53 cut_off_limit = 256000
53 cut_off_limit = 256000
54 force_https = false
54 force_https = false
55 commit_parse_limit = 50
55 commit_parse_limit = 50
56 use_gravatar = true
56 use_gravatar = true
57 container_auth_enabled = false
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
58 proxypass_auth_enabled = false
59
59
60 ## overwrite schema of clone url
60 ## overwrite schema of clone url
61 # available vars:
61 ## available vars:
62 # scheme - http/https
62 ## scheme - http/https
63 # user - current user
63 ## user - current user
64 # pass - password
64 ## pass - password
65 # netloc - network location
65 ## netloc - network location
66 # path - usually repo_name
66 ## path - usually repo_name
67
67 # clone_uri = {scheme}://{user}{pass}{netloc}{path}
68 # clone_uri = {scheme}://{user}{pass}{netloc}{path}
68
69
70 ## issue tracking mapping for commits messages
71 ## uncomment url_pat, issue_server, issue_prefix to enable
72
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #1234
76
77 #url_pat = (?:^#|\s#)(\w+)
78
79 ## server url to the issue, each {id} will be replaced with id
80 ## fetched from the regex
81
82 #issue_server = https://myissueserver.com/issue/{id}
83
84 ## prefix to add to link to indicate it's an url
85 ## #314 will be replaced by <issue_prefix><id>
86
87 #issue_prefix = #
88
89
69 ####################################
90 ####################################
70 ### CELERY CONFIG ####
91 ### CELERY CONFIG ####
71 ####################################
92 ####################################
72 use_celery = false
93 use_celery = false
73 broker.host = localhost
94 broker.host = localhost
74 broker.vhost = rabbitmqhost
95 broker.vhost = rabbitmqhost
75 broker.port = 5672
96 broker.port = 5672
76 broker.user = rabbitmq
97 broker.user = rabbitmq
77 broker.password = qweqwe
98 broker.password = qweqwe
78
99
79 celery.imports = rhodecode.lib.celerylib.tasks
100 celery.imports = rhodecode.lib.celerylib.tasks
80
101
81 celery.result.backend = amqp
102 celery.result.backend = amqp
82 celery.result.dburi = amqp://
103 celery.result.dburi = amqp://
83 celery.result.serialier = json
104 celery.result.serialier = json
84
105
85 #celery.send.task.error.emails = true
106 #celery.send.task.error.emails = true
86 #celery.amqp.task.result.expires = 18000
107 #celery.amqp.task.result.expires = 18000
87
108
88 celeryd.concurrency = 2
109 celeryd.concurrency = 2
89 #celeryd.log.file = celeryd.log
110 #celeryd.log.file = celeryd.log
90 celeryd.log.level = debug
111 celeryd.log.level = debug
91 celeryd.max.tasks.per.child = 1
112 celeryd.max.tasks.per.child = 1
92
113
93 #tasks will never be sent to the queue, but executed locally instead.
114 #tasks will never be sent to the queue, but executed locally instead.
94 celery.always.eager = false
115 celery.always.eager = false
95
116
96 ####################################
117 ####################################
97 ### BEAKER CACHE ####
118 ### BEAKER CACHE ####
98 ####################################
119 ####################################
99 beaker.cache.data_dir=%(here)s/data/cache/data
120 beaker.cache.data_dir=%(here)s/data/cache/data
100 beaker.cache.lock_dir=%(here)s/data/cache/lock
121 beaker.cache.lock_dir=%(here)s/data/cache/lock
101
122
102 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
123 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
103
124
104 beaker.cache.super_short_term.type=memory
125 beaker.cache.super_short_term.type=memory
105 beaker.cache.super_short_term.expire=10
126 beaker.cache.super_short_term.expire=10
106 beaker.cache.super_short_term.key_length = 256
127 beaker.cache.super_short_term.key_length = 256
107
128
108 beaker.cache.short_term.type=memory
129 beaker.cache.short_term.type=memory
109 beaker.cache.short_term.expire=60
130 beaker.cache.short_term.expire=60
110 beaker.cache.short_term.key_length = 256
131 beaker.cache.short_term.key_length = 256
111
132
112 beaker.cache.long_term.type=memory
133 beaker.cache.long_term.type=memory
113 beaker.cache.long_term.expire=36000
134 beaker.cache.long_term.expire=36000
114 beaker.cache.long_term.key_length = 256
135 beaker.cache.long_term.key_length = 256
115
136
116 beaker.cache.sql_cache_short.type=memory
137 beaker.cache.sql_cache_short.type=memory
117 beaker.cache.sql_cache_short.expire=10
138 beaker.cache.sql_cache_short.expire=10
118 beaker.cache.sql_cache_short.key_length = 256
139 beaker.cache.sql_cache_short.key_length = 256
119
140
120 beaker.cache.sql_cache_med.type=memory
141 beaker.cache.sql_cache_med.type=memory
121 beaker.cache.sql_cache_med.expire=360
142 beaker.cache.sql_cache_med.expire=360
122 beaker.cache.sql_cache_med.key_length = 256
143 beaker.cache.sql_cache_med.key_length = 256
123
144
124 beaker.cache.sql_cache_long.type=file
145 beaker.cache.sql_cache_long.type=file
125 beaker.cache.sql_cache_long.expire=3600
146 beaker.cache.sql_cache_long.expire=3600
126 beaker.cache.sql_cache_long.key_length = 256
147 beaker.cache.sql_cache_long.key_length = 256
127
148
128 ####################################
149 ####################################
129 ### BEAKER SESSION ####
150 ### BEAKER SESSION ####
130 ####################################
151 ####################################
131 ## Type of storage used for the session, current types are
152 ## Type of storage used for the session, current types are
132 ## dbm, file, memcached, database, and memory.
153 ## dbm, file, memcached, database, and memory.
133 ## The storage uses the Container API
154 ## The storage uses the Container API
134 ## that is also used by the cache system.
155 ## that is also used by the cache system.
135
156
136 ## db session example
157 ## db session example
137
158
138 #beaker.session.type = ext:database
159 #beaker.session.type = ext:database
139 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
160 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
140 #beaker.session.table_name = db_session
161 #beaker.session.table_name = db_session
141
162
142 ## encrypted cookie session, good for many instances
163 ## encrypted cookie session, good for many instances
143 #beaker.session.type = cookie
164 #beaker.session.type = cookie
144
165
145 beaker.session.type = file
166 beaker.session.type = file
146 beaker.session.key = rhodecode
167 beaker.session.key = rhodecode
147 beaker.session.encrypt_key = ${app_instance_secret}
168 beaker.session.encrypt_key = ${app_instance_secret}
148 beaker.session.validate_key = ${app_instance_secret}
169 beaker.session.validate_key = ${app_instance_secret}
149 beaker.session.timeout = 36000
170 beaker.session.timeout = 36000
150 beaker.session.httponly = true
171 beaker.session.httponly = true
151
172
152 ## uncomment for https secure cookie
173 ## uncomment for https secure cookie
153 beaker.session.secure = false
174 beaker.session.secure = false
154
175
155 ##auto save the session to not to use .save()
176 ##auto save the session to not to use .save()
156 beaker.session.auto = False
177 beaker.session.auto = False
157
178
158 ##true exire at browser close
179 ##true exire at browser close
159 #beaker.session.cookie_expires = 3600
180 #beaker.session.cookie_expires = 3600
160
181
161
182
162 ################################################################################
183 ################################################################################
163 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
184 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
164 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
185 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
165 ## execute malicious code after an exception is raised. ##
186 ## execute malicious code after an exception is raised. ##
166 ################################################################################
187 ################################################################################
167 set debug = false
188 set debug = false
168
189
169 ##################################
190 ##################################
170 ### LOGVIEW CONFIG ###
191 ### LOGVIEW CONFIG ###
171 ##################################
192 ##################################
172 logview.sqlalchemy = #faa
193 logview.sqlalchemy = #faa
173 logview.pylons.templating = #bfb
194 logview.pylons.templating = #bfb
174 logview.pylons.util = #eee
195 logview.pylons.util = #eee
175
196
176 #########################################################
197 #########################################################
177 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
198 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
178 #########################################################
199 #########################################################
179
200
180 # SQLITE [default]
201 # SQLITE [default]
181 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
202 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
182
203
183 # POSTGRESQL
204 # POSTGRESQL
184 # sqlalchemy.db1.url = postgresql://user:pass@localhost/rhodecode
205 # sqlalchemy.db1.url = postgresql://user:pass@localhost/rhodecode
185
206
186 # MySQL
207 # MySQL
187 # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode
208 # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode
188
209
189 # see sqlalchemy docs for others
210 # see sqlalchemy docs for others
190
211
191 sqlalchemy.db1.echo = false
212 sqlalchemy.db1.echo = false
192 sqlalchemy.db1.pool_recycle = 3600
213 sqlalchemy.db1.pool_recycle = 3600
193 sqlalchemy.convert_unicode = true
214 sqlalchemy.convert_unicode = true
194
215
195 ################################
216 ################################
196 ### LOGGING CONFIGURATION ####
217 ### LOGGING CONFIGURATION ####
197 ################################
218 ################################
198 [loggers]
219 [loggers]
199 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
220 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
200
221
201 [handlers]
222 [handlers]
202 keys = console, console_sql
223 keys = console, console_sql
203
224
204 [formatters]
225 [formatters]
205 keys = generic, color_formatter, color_formatter_sql
226 keys = generic, color_formatter, color_formatter_sql
206
227
207 #############
228 #############
208 ## LOGGERS ##
229 ## LOGGERS ##
209 #############
230 #############
210 [logger_root]
231 [logger_root]
211 level = NOTSET
232 level = NOTSET
212 handlers = console
233 handlers = console
213
234
214 [logger_routes]
235 [logger_routes]
215 level = DEBUG
236 level = DEBUG
216 handlers =
237 handlers =
217 qualname = routes.middleware
238 qualname = routes.middleware
218 # "level = DEBUG" logs the route matched and routing variables.
239 # "level = DEBUG" logs the route matched and routing variables.
219 propagate = 1
240 propagate = 1
220
241
221 [logger_beaker]
242 [logger_beaker]
222 level = DEBUG
243 level = DEBUG
223 handlers =
244 handlers =
224 qualname = beaker.container
245 qualname = beaker.container
225 propagate = 1
246 propagate = 1
226
247
227 [logger_templates]
248 [logger_templates]
228 level = INFO
249 level = INFO
229 handlers =
250 handlers =
230 qualname = pylons.templating
251 qualname = pylons.templating
231 propagate = 1
252 propagate = 1
232
253
233 [logger_rhodecode]
254 [logger_rhodecode]
234 level = DEBUG
255 level = DEBUG
235 handlers =
256 handlers =
236 qualname = rhodecode
257 qualname = rhodecode
237 propagate = 1
258 propagate = 1
238
259
239 [logger_sqlalchemy]
260 [logger_sqlalchemy]
240 level = INFO
261 level = INFO
241 handlers = console_sql
262 handlers = console_sql
242 qualname = sqlalchemy.engine
263 qualname = sqlalchemy.engine
243 propagate = 0
264 propagate = 0
244
265
245 ##############
266 ##############
246 ## HANDLERS ##
267 ## HANDLERS ##
247 ##############
268 ##############
248
269
249 [handler_console]
270 [handler_console]
250 class = StreamHandler
271 class = StreamHandler
251 args = (sys.stderr,)
272 args = (sys.stderr,)
252 level = INFO
273 level = INFO
253 formatter = color_formatter
274 formatter = color_formatter
254
275
255 [handler_console_sql]
276 [handler_console_sql]
256 class = StreamHandler
277 class = StreamHandler
257 args = (sys.stderr,)
278 args = (sys.stderr,)
258 level = WARN
279 level = WARN
259 formatter = color_formatter_sql
280 formatter = color_formatter_sql
260
281
261 ################
282 ################
262 ## FORMATTERS ##
283 ## FORMATTERS ##
263 ################
284 ################
264
285
265 [formatter_generic]
286 [formatter_generic]
266 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
287 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
267 datefmt = %Y-%m-%d %H:%M:%S
288 datefmt = %Y-%m-%d %H:%M:%S
268
289
269 [formatter_color_formatter]
290 [formatter_color_formatter]
270 class=rhodecode.lib.colored_formatter.ColorFormatter
291 class=rhodecode.lib.colored_formatter.ColorFormatter
271 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
292 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
272 datefmt = %Y-%m-%d %H:%M:%S
293 datefmt = %Y-%m-%d %H:%M:%S
273
294
274 [formatter_color_formatter_sql]
295 [formatter_color_formatter_sql]
275 class=rhodecode.lib.colored_formatter.ColorFormatterSql
296 class=rhodecode.lib.colored_formatter.ColorFormatterSql
276 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
297 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
277 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
298 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,499 +1,510 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.api
3 rhodecode.controllers.api
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 API controller for RhodeCode
6 API controller for RhodeCode
7
7
8 :created_on: Aug 20, 2011
8 :created_on: Aug 20, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
16 # of the License or (at your opinion) any later version of the license.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
27
27
28 import traceback
28 import traceback
29 import logging
29 import logging
30
30
31 from sqlalchemy.orm.exc import NoResultFound
31 from sqlalchemy.orm.exc import NoResultFound
32
32
33 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
33 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
34 from rhodecode.lib.auth import HasPermissionAllDecorator, \
34 from rhodecode.lib.auth import HasPermissionAllDecorator, \
35 HasPermissionAnyDecorator
35 HasPermissionAnyDecorator
36
36
37 from rhodecode.model.meta import Session
37 from rhodecode.model.meta import Session
38 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
40 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.user import UserModel
41 from rhodecode.model.user import UserModel
42 from rhodecode.model.repo_permission import RepositoryPermissionModel
42 from rhodecode.model.repo_permission import RepositoryPermissionModel
43 from rhodecode.model.users_group import UsersGroupModel
43 from rhodecode.model.users_group import UsersGroupModel
44 from rhodecode.model.repos_group import ReposGroupModel
44 from rhodecode.model.repos_group import ReposGroupModel
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class ApiController(JSONRPCController):
50 class ApiController(JSONRPCController):
51 """
51 """
52 API Controller
52 API Controller
53
53
54
54
55 Each method needs to have USER as argument this is then based on given
55 Each method needs to have USER as argument this is then based on given
56 API_KEY propagated as instance of user object
56 API_KEY propagated as instance of user object
57
57
58 Preferably this should be first argument also
58 Preferably this should be first argument also
59
59
60
60
61 Each function should also **raise** JSONRPCError for any
61 Each function should also **raise** JSONRPCError for any
62 errors that happens
62 errors that happens
63
63
64 """
64 """
65
65
66 @HasPermissionAllDecorator('hg.admin')
66 @HasPermissionAllDecorator('hg.admin')
67 def pull(self, apiuser, repo):
67 def pull(self, apiuser, repo_name):
68 """
68 """
69 Dispatch pull action on given repo
69 Dispatch pull action on given repo
70
70
71
71
72 :param user:
72 :param user:
73 :param repo:
73 :param repo_name:
74 """
74 """
75
75
76 if Repository.is_valid(repo) is False:
76 if Repository.is_valid(repo_name) is False:
77 raise JSONRPCError('Unknown repo "%s"' % repo)
77 raise JSONRPCError('Unknown repo "%s"' % repo_name)
78
78
79 try:
79 try:
80 ScmModel().pull_changes(repo, self.rhodecode_user.username)
80 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
81 return 'Pulled from %s' % repo
81 return 'Pulled from %s' % repo_name
82 except Exception:
82 except Exception:
83 raise JSONRPCError('Unable to pull changes from "%s"' % repo)
83 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
84
84
85 @HasPermissionAllDecorator('hg.admin')
85 @HasPermissionAllDecorator('hg.admin')
86 def get_user(self, apiuser, username):
86 def get_user(self, apiuser, username):
87 """"
87 """"
88 Get a user by username
88 Get a user by username
89
89
90 :param apiuser:
90 :param apiuser:
91 :param username:
91 :param username:
92 """
92 """
93
93
94 user = User.get_by_username(username)
94 user = User.get_by_username(username)
95 if not user:
95 if not user:
96 return None
96 return None
97
97
98 return dict(
98 return dict(
99 id=user.user_id,
99 id=user.user_id,
100 username=user.username,
100 username=user.username,
101 firstname=user.name,
101 firstname=user.name,
102 lastname=user.lastname,
102 lastname=user.lastname,
103 email=user.email,
103 email=user.email,
104 active=user.active,
104 active=user.active,
105 admin=user.admin,
105 admin=user.admin,
106 ldap=user.ldap_dn
106 ldap=user.ldap_dn
107 )
107 )
108
108
109 @HasPermissionAllDecorator('hg.admin')
109 @HasPermissionAllDecorator('hg.admin')
110 def get_users(self, apiuser):
110 def get_users(self, apiuser):
111 """"
111 """"
112 Get all users
112 Get all users
113
113
114 :param apiuser:
114 :param apiuser:
115 """
115 """
116
116
117 result = []
117 result = []
118 for user in User.getAll():
118 for user in User.getAll():
119 result.append(
119 result.append(
120 dict(
120 dict(
121 id=user.user_id,
121 id=user.user_id,
122 username=user.username,
122 username=user.username,
123 firstname=user.name,
123 firstname=user.name,
124 lastname=user.lastname,
124 lastname=user.lastname,
125 email=user.email,
125 email=user.email,
126 active=user.active,
126 active=user.active,
127 admin=user.admin,
127 admin=user.admin,
128 ldap=user.ldap_dn
128 ldap=user.ldap_dn
129 )
129 )
130 )
130 )
131 return result
131 return result
132
132
133 @HasPermissionAllDecorator('hg.admin')
133 @HasPermissionAllDecorator('hg.admin')
134 def create_user(self, apiuser, username, password, firstname,
134 def create_user(self, apiuser, username, password, firstname,
135 lastname, email, active=True, admin=False, ldap_dn=None):
135 lastname, email, active=True, admin=False, ldap_dn=None):
136 """
136 """
137 Create new user
137 Create new user
138
138
139 :param apiuser:
139 :param apiuser:
140 :param username:
140 :param username:
141 :param password:
141 :param password:
142 :param name:
142 :param name:
143 :param lastname:
143 :param lastname:
144 :param email:
144 :param email:
145 :param active:
145 :param active:
146 :param admin:
146 :param admin:
147 :param ldap_dn:
147 :param ldap_dn:
148 """
148 """
149
149
150 if User.get_by_username(username):
150 if User.get_by_username(username):
151 raise JSONRPCError("user %s already exist" % username)
151 raise JSONRPCError("user %s already exist" % username)
152
152
153 try:
153 try:
154 UserModel().create_or_update(username, password, email, firstname,
154 usr = UserModel().create_or_update(
155 lastname, active, admin, ldap_dn)
155 username, password, email, firstname,
156 lastname, active, admin, ldap_dn
157 )
156 Session.commit()
158 Session.commit()
157 return dict(msg='created new user %s' % username)
159 return dict(
160 id=usr.user_id,
161 msg='created new user %s' % username
162 )
158 except Exception:
163 except Exception:
159 log.error(traceback.format_exc())
164 log.error(traceback.format_exc())
160 raise JSONRPCError('failed to create user %s' % username)
165 raise JSONRPCError('failed to create user %s' % username)
161
166
162 @HasPermissionAllDecorator('hg.admin')
167 @HasPermissionAllDecorator('hg.admin')
163 def get_users_group(self, apiuser, group_name):
168 def get_users_group(self, apiuser, group_name):
164 """"
169 """"
165 Get users group by name
170 Get users group by name
166
171
167 :param apiuser:
172 :param apiuser:
168 :param group_name:
173 :param group_name:
169 """
174 """
170
175
171 users_group = UsersGroup.get_by_group_name(group_name)
176 users_group = UsersGroup.get_by_group_name(group_name)
172 if not users_group:
177 if not users_group:
173 return None
178 return None
174
179
175 members = []
180 members = []
176 for user in users_group.members:
181 for user in users_group.members:
177 user = user.user
182 user = user.user
178 members.append(dict(id=user.user_id,
183 members.append(dict(id=user.user_id,
179 username=user.username,
184 username=user.username,
180 firstname=user.name,
185 firstname=user.name,
181 lastname=user.lastname,
186 lastname=user.lastname,
182 email=user.email,
187 email=user.email,
183 active=user.active,
188 active=user.active,
184 admin=user.admin,
189 admin=user.admin,
185 ldap=user.ldap_dn))
190 ldap=user.ldap_dn))
186
191
187 return dict(id=users_group.users_group_id,
192 return dict(id=users_group.users_group_id,
188 name=users_group.users_group_name,
193 group_name=users_group.users_group_name,
189 active=users_group.users_group_active,
194 active=users_group.users_group_active,
190 members=members)
195 members=members)
191
196
192 @HasPermissionAllDecorator('hg.admin')
197 @HasPermissionAllDecorator('hg.admin')
193 def get_users_groups(self, apiuser):
198 def get_users_groups(self, apiuser):
194 """"
199 """"
195 Get all users groups
200 Get all users groups
196
201
197 :param apiuser:
202 :param apiuser:
198 """
203 """
199
204
200 result = []
205 result = []
201 for users_group in UsersGroup.getAll():
206 for users_group in UsersGroup.getAll():
202 members = []
207 members = []
203 for user in users_group.members:
208 for user in users_group.members:
204 user = user.user
209 user = user.user
205 members.append(dict(id=user.user_id,
210 members.append(dict(id=user.user_id,
206 username=user.username,
211 username=user.username,
207 firstname=user.name,
212 firstname=user.name,
208 lastname=user.lastname,
213 lastname=user.lastname,
209 email=user.email,
214 email=user.email,
210 active=user.active,
215 active=user.active,
211 admin=user.admin,
216 admin=user.admin,
212 ldap=user.ldap_dn))
217 ldap=user.ldap_dn))
213
218
214 result.append(dict(id=users_group.users_group_id,
219 result.append(dict(id=users_group.users_group_id,
215 name=users_group.users_group_name,
220 group_name=users_group.users_group_name,
216 active=users_group.users_group_active,
221 active=users_group.users_group_active,
217 members=members))
222 members=members))
218 return result
223 return result
219
224
220 @HasPermissionAllDecorator('hg.admin')
225 @HasPermissionAllDecorator('hg.admin')
221 def create_users_group(self, apiuser, name, active=True):
226 def create_users_group(self, apiuser, group_name, active=True):
222 """
227 """
223 Creates an new usergroup
228 Creates an new usergroup
224
229
225 :param name:
230 :param group_name:
226 :param active:
231 :param active:
227 """
232 """
228
233
229 if self.get_users_group(apiuser, name):
234 if self.get_users_group(apiuser, group_name):
230 raise JSONRPCError("users group %s already exist" % name)
235 raise JSONRPCError("users group %s already exist" % group_name)
231
236
232 try:
237 try:
233 ug = UsersGroupModel().create(name=name, active=active)
238 ug = UsersGroupModel().create(name=group_name, active=active)
234 Session.commit()
239 Session.commit()
235 return dict(id=ug.users_group_id,
240 return dict(id=ug.users_group_id,
236 msg='created new users group %s' % name)
241 msg='created new users group %s' % group_name)
237 except Exception:
242 except Exception:
238 log.error(traceback.format_exc())
243 log.error(traceback.format_exc())
239 raise JSONRPCError('failed to create group %s' % name)
244 raise JSONRPCError('failed to create group %s' % group_name)
240
245
241 @HasPermissionAllDecorator('hg.admin')
246 @HasPermissionAllDecorator('hg.admin')
242 def add_user_to_users_group(self, apiuser, group_name, username):
247 def add_user_to_users_group(self, apiuser, group_name, username):
243 """"
248 """"
244 Add a user to a group
249 Add a user to a group
245
250
246 :param apiuser:
251 :param apiuser:
247 :param group_name:
252 :param group_name:
248 :param username:
253 :param username:
249 """
254 """
250
255
251 try:
256 try:
252 users_group = UsersGroup.get_by_group_name(group_name)
257 users_group = UsersGroup.get_by_group_name(group_name)
253 if not users_group:
258 if not users_group:
254 raise JSONRPCError('unknown users group %s' % group_name)
259 raise JSONRPCError('unknown users group %s' % group_name)
255
260
256 try:
261 try:
257 user = User.get_by_username(username)
262 user = User.get_by_username(username)
258 except NoResultFound:
263 except NoResultFound:
259 raise JSONRPCError('unknown user %s' % username)
264 raise JSONRPCError('unknown user %s' % username)
260
265
261 ugm = UsersGroupModel().add_user_to_group(users_group, user)
266 ugm = UsersGroupModel().add_user_to_group(users_group, user)
262 Session.commit()
267 Session.commit()
263 return dict(id=ugm.users_group_member_id,
268 return dict(id=ugm.users_group_member_id,
264 msg='created new users group member')
269 msg='created new users group member')
265 except Exception:
270 except Exception:
266 log.error(traceback.format_exc())
271 log.error(traceback.format_exc())
267 raise JSONRPCError('failed to create users group member')
272 raise JSONRPCError('failed to create users group member')
268
273
269 @HasPermissionAnyDecorator('hg.admin')
274 @HasPermissionAnyDecorator('hg.admin')
270 def get_repo(self, apiuser, repo_name):
275 def get_repo(self, apiuser, repo_name):
271 """"
276 """"
272 Get repository by name
277 Get repository by name
273
278
274 :param apiuser:
279 :param apiuser:
275 :param repo_name:
280 :param repo_name:
276 """
281 """
277
282
278 repo = Repository.get_by_repo_name(repo_name)
283 repo = Repository.get_by_repo_name(repo_name)
279 if repo is None:
284 if repo is None:
280 raise JSONRPCError('unknown repository %s' % repo)
285 raise JSONRPCError('unknown repository %s' % repo)
281
286
282 members = []
287 members = []
283 for user in repo.repo_to_perm:
288 for user in repo.repo_to_perm:
284 perm = user.permission.permission_name
289 perm = user.permission.permission_name
285 user = user.user
290 user = user.user
286 members.append(
291 members.append(
287 dict(
292 dict(
288 type_="user",
293 type_="user",
289 id=user.user_id,
294 id=user.user_id,
290 username=user.username,
295 username=user.username,
291 firstname=user.name,
296 firstname=user.name,
292 lastname=user.lastname,
297 lastname=user.lastname,
293 email=user.email,
298 email=user.email,
294 active=user.active,
299 active=user.active,
295 admin=user.admin,
300 admin=user.admin,
296 ldap=user.ldap_dn,
301 ldap=user.ldap_dn,
297 permission=perm
302 permission=perm
298 )
303 )
299 )
304 )
300 for users_group in repo.users_group_to_perm:
305 for users_group in repo.users_group_to_perm:
301 perm = users_group.permission.permission_name
306 perm = users_group.permission.permission_name
302 users_group = users_group.users_group
307 users_group = users_group.users_group
303 members.append(
308 members.append(
304 dict(
309 dict(
305 type_="users_group",
310 type_="users_group",
306 id=users_group.users_group_id,
311 id=users_group.users_group_id,
307 name=users_group.users_group_name,
312 name=users_group.users_group_name,
308 active=users_group.users_group_active,
313 active=users_group.users_group_active,
309 permission=perm
314 permission=perm
310 )
315 )
311 )
316 )
312
317
313 return dict(
318 return dict(
314 id=repo.repo_id,
319 id=repo.repo_id,
315 name=repo.repo_name,
320 repo_name=repo.repo_name,
316 type=repo.repo_type,
321 type=repo.repo_type,
317 description=repo.description,
322 description=repo.description,
318 members=members
323 members=members
319 )
324 )
320
325
321 @HasPermissionAnyDecorator('hg.admin')
326 @HasPermissionAnyDecorator('hg.admin')
322 def get_repos(self, apiuser):
327 def get_repos(self, apiuser):
323 """"
328 """"
324 Get all repositories
329 Get all repositories
325
330
326 :param apiuser:
331 :param apiuser:
327 """
332 """
328
333
329 result = []
334 result = []
330 for repository in Repository.getAll():
335 for repository in Repository.getAll():
331 result.append(
336 result.append(
332 dict(
337 dict(
333 id=repository.repo_id,
338 id=repository.repo_id,
334 name=repository.repo_name,
339 repo_name=repository.repo_name,
335 type=repository.repo_type,
340 type=repository.repo_type,
336 description=repository.description
341 description=repository.description
337 )
342 )
338 )
343 )
339 return result
344 return result
340
345
341 @HasPermissionAnyDecorator('hg.admin')
346 @HasPermissionAnyDecorator('hg.admin')
342 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
347 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
343 ret_type='all'):
348 ret_type='all'):
344 """
349 """
345 returns a list of nodes and it's children
350 returns a list of nodes and it's children
346 for a given path at given revision. It's possible to specify ret_type
351 for a given path at given revision. It's possible to specify ret_type
347 to show only files or dirs
352 to show only files or dirs
348
353
349 :param apiuser:
354 :param apiuser:
350 :param repo_name: name of repository
355 :param repo_name: name of repository
351 :param revision: revision for which listing should be done
356 :param revision: revision for which listing should be done
352 :param root_path: path from which start displaying
357 :param root_path: path from which start displaying
353 :param ret_type: return type 'all|files|dirs' nodes
358 :param ret_type: return type 'all|files|dirs' nodes
354 """
359 """
355 try:
360 try:
356 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
361 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
357 flat=False)
362 flat=False)
358 _map = {
363 _map = {
359 'all': _d + _f,
364 'all': _d + _f,
360 'files': _f,
365 'files': _f,
361 'dirs': _d,
366 'dirs': _d,
362 }
367 }
363 return _map[ret_type]
368 return _map[ret_type]
364 except KeyError:
369 except KeyError:
365 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
370 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
366 except Exception, e:
371 except Exception, e:
367 raise JSONRPCError(e)
372 raise JSONRPCError(e)
368
373
369 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
374 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
370 def create_repo(self, apiuser, name, owner_name, description='',
375 def create_repo(self, apiuser, repo_name, owner_name, description='',
371 repo_type='hg', private=False):
376 repo_type='hg', private=False):
372 """
377 """
373 Create a repository
378 Create a repository
374
379
375 :param apiuser:
380 :param apiuser:
376 :param name:
381 :param repo_name:
377 :param description:
382 :param description:
378 :param type:
383 :param type:
379 :param private:
384 :param private:
380 :param owner_name:
385 :param owner_name:
381 """
386 """
382
387
383 try:
388 try:
384 try:
389 try:
385 owner = User.get_by_username(owner_name)
390 owner = User.get_by_username(owner_name)
386 except NoResultFound:
391 except NoResultFound:
387 raise JSONRPCError('unknown user %s' % owner)
392 raise JSONRPCError('unknown user %s' % owner)
388
393
389 if self.get_repo(apiuser, name):
394 if Repository.get_by_repo_name(repo_name):
390 raise JSONRPCError("repo %s already exist" % name)
395 raise JSONRPCError("repo %s already exist" % repo_name)
391
396
392 groups = name.split('/')
397 groups = repo_name.split('/')
393 real_name = groups[-1]
398 real_name = groups[-1]
394 groups = groups[:-1]
399 groups = groups[:-1]
395 parent_id = None
400 parent_id = None
396 for g in groups:
401 for g in groups:
397 group = RepoGroup.get_by_group_name(g)
402 group = RepoGroup.get_by_group_name(g)
398 if not group:
403 if not group:
399 group = ReposGroupModel().create(
404 group = ReposGroupModel().create(
400 dict(
405 dict(
401 group_name=g,
406 group_name=g,
402 group_description='',
407 group_description='',
403 group_parent_id=parent_id
408 group_parent_id=parent_id
404 )
409 )
405 )
410 )
406 parent_id = group.group_id
411 parent_id = group.group_id
407
412
408 RepoModel().create(
413 repo = RepoModel().create(
409 dict(
414 dict(
410 repo_name=real_name,
415 repo_name=real_name,
411 repo_name_full=name,
416 repo_name_full=repo_name,
412 description=description,
417 description=description,
413 private=private,
418 private=private,
414 repo_type=repo_type,
419 repo_type=repo_type,
415 repo_group=parent_id,
420 repo_group=parent_id,
416 clone_uri=None
421 clone_uri=None
417 ),
422 ),
418 owner
423 owner
419 )
424 )
420 Session.commit()
425 Session.commit()
426
427 return dict(
428 id=repo.repo_id,
429 msg="Created new repository %s" % repo.repo_name
430 )
431
421 except Exception:
432 except Exception:
422 log.error(traceback.format_exc())
433 log.error(traceback.format_exc())
423 raise JSONRPCError('failed to create repository %s' % name)
434 raise JSONRPCError('failed to create repository %s' % repo_name)
424
435
425 @HasPermissionAnyDecorator('hg.admin')
436 @HasPermissionAnyDecorator('hg.admin')
426 def add_user_to_repo(self, apiuser, repo_name, username, perm):
437 def add_user_to_repo(self, apiuser, repo_name, username, perm):
427 """
438 """
428 Add permission for a user to a repository
439 Add permission for a user to a repository
429
440
430 :param apiuser:
441 :param apiuser:
431 :param repo_name:
442 :param repo_name:
432 :param username:
443 :param username:
433 :param perm:
444 :param perm:
434 """
445 """
435
446
436 try:
447 try:
437 repo = Repository.get_by_repo_name(repo_name)
448 repo = Repository.get_by_repo_name(repo_name)
438 if repo is None:
449 if repo is None:
439 raise JSONRPCError('unknown repository %s' % repo)
450 raise JSONRPCError('unknown repository %s' % repo)
440
451
441 try:
452 try:
442 user = User.get_by_username(username)
453 user = User.get_by_username(username)
443 except NoResultFound:
454 except NoResultFound:
444 raise JSONRPCError('unknown user %s' % user)
455 raise JSONRPCError('unknown user %s' % user)
445
456
446 RepositoryPermissionModel()\
457 RepositoryPermissionModel()\
447 .update_or_delete_user_permission(repo, user, perm)
458 .update_or_delete_user_permission(repo, user, perm)
448 Session.commit()
459 Session.commit()
449
460
450 return dict(
461 return dict(
451 msg='Added perm: %s for %s in repo: %s' % (
462 msg='Added perm: %s for %s in repo: %s' % (
452 perm, username, repo_name
463 perm, username, repo_name
453 )
464 )
454 )
465 )
455 except Exception:
466 except Exception:
456 log.error(traceback.format_exc())
467 log.error(traceback.format_exc())
457 raise JSONRPCError(
468 raise JSONRPCError(
458 'failed to edit permission %(repo)s for %(user)s' % dict(
469 'failed to edit permission %(repo)s for %(user)s' % dict(
459 user=username, repo=repo_name
470 user=username, repo=repo_name
460 )
471 )
461 )
472 )
462
473
463 @HasPermissionAnyDecorator('hg.admin')
474 @HasPermissionAnyDecorator('hg.admin')
464 def add_users_group_to_repo(self, apiuser, repo_name, group_name, perm):
475 def add_users_group_to_repo(self, apiuser, repo_name, group_name, perm):
465 """
476 """
466 Add permission for a users group to a repository
477 Add permission for a users group to a repository
467
478
468 :param apiuser:
479 :param apiuser:
469 :param repo_name:
480 :param repo_name:
470 :param group_name:
481 :param group_name:
471 :param perm:
482 :param perm:
472 """
483 """
473
484
474 try:
485 try:
475 repo = Repository.get_by_repo_name(repo_name)
486 repo = Repository.get_by_repo_name(repo_name)
476 if repo is None:
487 if repo is None:
477 raise JSONRPCError('unknown repository %s' % repo)
488 raise JSONRPCError('unknown repository %s' % repo)
478
489
479 try:
490 try:
480 user_group = UsersGroup.get_by_group_name(group_name)
491 user_group = UsersGroup.get_by_group_name(group_name)
481 except NoResultFound:
492 except NoResultFound:
482 raise JSONRPCError('unknown users group %s' % user_group)
493 raise JSONRPCError('unknown users group %s' % user_group)
483
494
484 RepositoryPermissionModel()\
495 RepositoryPermissionModel()\
485 .update_or_delete_users_group_permission(repo, user_group,
496 .update_or_delete_users_group_permission(repo, user_group,
486 perm)
497 perm)
487 Session.commit()
498 Session.commit()
488 return dict(
499 return dict(
489 msg='Added perm: %s for %s in repo: %s' % (
500 msg='Added perm: %s for %s in repo: %s' % (
490 perm, group_name, repo_name
501 perm, group_name, repo_name
491 )
502 )
492 )
503 )
493 except Exception:
504 except Exception:
494 log.error(traceback.format_exc())
505 log.error(traceback.format_exc())
495 raise JSONRPCError(
506 raise JSONRPCError(
496 'failed to edit permission %(repo)s for %(usergr)s' % dict(
507 'failed to edit permission %(repo)s for %(usergr)s' % dict(
497 usergr=group_name, repo=repo_name
508 usergr=group_name, repo=repo_name
498 )
509 )
499 )
510 )
@@ -1,482 +1,492 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.db_manage
3 rhodecode.lib.db_manage
4 ~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Database creation, and setup module for RhodeCode. Used for creation
6 Database creation, and setup module for RhodeCode. Used for creation
7 of database as well as for migration operations
7 of database as well as for migration operations
8
8
9 :created_on: Apr 10, 2010
9 :created_on: Apr 10, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import uuid
29 import uuid
30 import logging
30 import logging
31 from os.path import dirname as dn, join as jn
31 from os.path import dirname as dn, join as jn
32
32
33 from rhodecode import __dbversion__
33 from rhodecode import __dbversion__
34 from rhodecode.model import meta
34 from rhodecode.model import meta
35
35
36 from rhodecode.model.user import UserModel
36 from rhodecode.model.user import UserModel
37 from rhodecode.lib.utils import ask_ok
37 from rhodecode.lib.utils import ask_ok
38 from rhodecode.model import init_model
38 from rhodecode.model import init_model
39 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
39 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
40 RhodeCodeSetting, UserToPerm, DbMigrateVersion
40 RhodeCodeSetting, UserToPerm, DbMigrateVersion
41
41
42 from sqlalchemy.engine import create_engine
42 from sqlalchemy.engine import create_engine
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class DbManage(object):
47 class DbManage(object):
48 def __init__(self, log_sql, dbconf, root, tests=False):
48 def __init__(self, log_sql, dbconf, root, tests=False):
49 self.dbname = dbconf.split('/')[-1]
49 self.dbname = dbconf.split('/')[-1]
50 self.tests = tests
50 self.tests = tests
51 self.root = root
51 self.root = root
52 self.dburi = dbconf
52 self.dburi = dbconf
53 self.log_sql = log_sql
53 self.log_sql = log_sql
54 self.db_exists = False
54 self.db_exists = False
55 self.init_db()
55 self.init_db()
56
56
57 def init_db(self):
57 def init_db(self):
58 engine = create_engine(self.dburi, echo=self.log_sql)
58 engine = create_engine(self.dburi, echo=self.log_sql)
59 init_model(engine)
59 init_model(engine)
60 self.sa = meta.Session
60 self.sa = meta.Session
61
61
62 def create_tables(self, override=False):
62 def create_tables(self, override=False):
63 """
63 """
64 Create a auth database
64 Create a auth database
65 """
65 """
66
66
67 log.info("Any existing database is going to be destroyed")
67 log.info("Any existing database is going to be destroyed")
68 if self.tests:
68 if self.tests:
69 destroy = True
69 destroy = True
70 else:
70 else:
71 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
71 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
72 if not destroy:
72 if not destroy:
73 sys.exit()
73 sys.exit()
74 if destroy:
74 if destroy:
75 meta.Base.metadata.drop_all()
75 meta.Base.metadata.drop_all()
76
76
77 checkfirst = not override
77 checkfirst = not override
78 meta.Base.metadata.create_all(checkfirst=checkfirst)
78 meta.Base.metadata.create_all(checkfirst=checkfirst)
79 log.info('Created tables for %s', self.dbname)
79 log.info('Created tables for %s', self.dbname)
80
80
81 def set_db_version(self):
81 def set_db_version(self):
82 ver = DbMigrateVersion()
82 ver = DbMigrateVersion()
83 ver.version = __dbversion__
83 ver.version = __dbversion__
84 ver.repository_id = 'rhodecode_db_migrations'
84 ver.repository_id = 'rhodecode_db_migrations'
85 ver.repository_path = 'versions'
85 ver.repository_path = 'versions'
86 self.sa.add(ver)
86 self.sa.add(ver)
87 log.info('db version set to: %s', __dbversion__)
87 log.info('db version set to: %s', __dbversion__)
88
88
89 def upgrade(self):
89 def upgrade(self):
90 """
90 """
91 Upgrades given database schema to given revision following
91 Upgrades given database schema to given revision following
92 all needed steps, to perform the upgrade
92 all needed steps, to perform the upgrade
93
93
94 """
94 """
95
95
96 from rhodecode.lib.dbmigrate.migrate.versioning import api
96 from rhodecode.lib.dbmigrate.migrate.versioning import api
97 from rhodecode.lib.dbmigrate.migrate.exceptions import \
97 from rhodecode.lib.dbmigrate.migrate.exceptions import \
98 DatabaseNotControlledError
98 DatabaseNotControlledError
99
99
100 if 'sqlite' in self.dburi:
101 print (
102 '********************** WARNING **********************\n'
103 'Make sure your version of sqlite is at least 3.7.X. \n'
104 'Earlier versions are known to fail on some migrations\n'
105 '*****************************************************\n'
106 )
100 upgrade = ask_ok('You are about to perform database upgrade, make '
107 upgrade = ask_ok('You are about to perform database upgrade, make '
101 'sure You backed up your database before. '
108 'sure You backed up your database before. '
102 'Continue ? [y/n]')
109 'Continue ? [y/n]')
103 if not upgrade:
110 if not upgrade:
104 sys.exit('Nothing done')
111 sys.exit('Nothing done')
105
112
106 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
113 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
107 'rhodecode/lib/dbmigrate')
114 'rhodecode/lib/dbmigrate')
108 db_uri = self.dburi
115 db_uri = self.dburi
109
116
110 try:
117 try:
111 curr_version = api.db_version(db_uri, repository_path)
118 curr_version = api.db_version(db_uri, repository_path)
112 msg = ('Found current database under version'
119 msg = ('Found current database under version'
113 ' control with version %s' % curr_version)
120 ' control with version %s' % curr_version)
114
121
115 except (RuntimeError, DatabaseNotControlledError):
122 except (RuntimeError, DatabaseNotControlledError):
116 curr_version = 1
123 curr_version = 1
117 msg = ('Current database is not under version control. Setting'
124 msg = ('Current database is not under version control. Setting'
118 ' as version %s' % curr_version)
125 ' as version %s' % curr_version)
119 api.version_control(db_uri, repository_path, curr_version)
126 api.version_control(db_uri, repository_path, curr_version)
120
127
121 print (msg)
128 print (msg)
122
129
123 if curr_version == __dbversion__:
130 if curr_version == __dbversion__:
124 sys.exit('This database is already at the newest version')
131 sys.exit('This database is already at the newest version')
125
132
126 #======================================================================
133 #======================================================================
127 # UPGRADE STEPS
134 # UPGRADE STEPS
128 #======================================================================
135 #======================================================================
129 class UpgradeSteps(object):
136 class UpgradeSteps(object):
130 """
137 """
131 Those steps follow schema versions so for example schema
138 Those steps follow schema versions so for example schema
132 for example schema with seq 002 == step_2 and so on.
139 for example schema with seq 002 == step_2 and so on.
133 """
140 """
134
141
135 def __init__(self, klass):
142 def __init__(self, klass):
136 self.klass = klass
143 self.klass = klass
137
144
138 def step_0(self):
145 def step_0(self):
139 # step 0 is the schema upgrade, and than follow proper upgrades
146 # step 0 is the schema upgrade, and than follow proper upgrades
140 print ('attempting to do database upgrade to version %s' \
147 print ('attempting to do database upgrade to version %s' \
141 % __dbversion__)
148 % __dbversion__)
142 api.upgrade(db_uri, repository_path, __dbversion__)
149 api.upgrade(db_uri, repository_path, __dbversion__)
143 print ('Schema upgrade completed')
150 print ('Schema upgrade completed')
144
151
145 def step_1(self):
152 def step_1(self):
146 pass
153 pass
147
154
148 def step_2(self):
155 def step_2(self):
149 print ('Patching repo paths for newer version of RhodeCode')
156 print ('Patching repo paths for newer version of RhodeCode')
150 self.klass.fix_repo_paths()
157 self.klass.fix_repo_paths()
151
158
152 print ('Patching default user of RhodeCode')
159 print ('Patching default user of RhodeCode')
153 self.klass.fix_default_user()
160 self.klass.fix_default_user()
154
161
155 log.info('Changing ui settings')
162 log.info('Changing ui settings')
156 self.klass.create_ui_settings()
163 self.klass.create_ui_settings()
157
164
158 def step_3(self):
165 def step_3(self):
159 print ('Adding additional settings into RhodeCode db')
166 print ('Adding additional settings into RhodeCode db')
160 self.klass.fix_settings()
167 self.klass.fix_settings()
161 print ('Adding ldap defaults')
168 print ('Adding ldap defaults')
162 self.klass.create_ldap_options(skip_existing=True)
169 self.klass.create_ldap_options(skip_existing=True)
163
170
171 def step_4(self):
172 print ('TODO:')
173
164 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
174 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
165
175
166 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
176 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
167 for step in upgrade_steps:
177 for step in upgrade_steps:
168 print ('performing upgrade step %s' % step)
178 print ('performing upgrade step %s' % step)
169 getattr(UpgradeSteps(self), 'step_%s' % step)()
179 getattr(UpgradeSteps(self), 'step_%s' % step)()
170
180
171 def fix_repo_paths(self):
181 def fix_repo_paths(self):
172 """
182 """
173 Fixes a old rhodecode version path into new one without a '*'
183 Fixes a old rhodecode version path into new one without a '*'
174 """
184 """
175
185
176 paths = self.sa.query(RhodeCodeUi)\
186 paths = self.sa.query(RhodeCodeUi)\
177 .filter(RhodeCodeUi.ui_key == '/')\
187 .filter(RhodeCodeUi.ui_key == '/')\
178 .scalar()
188 .scalar()
179
189
180 paths.ui_value = paths.ui_value.replace('*', '')
190 paths.ui_value = paths.ui_value.replace('*', '')
181
191
182 try:
192 try:
183 self.sa.add(paths)
193 self.sa.add(paths)
184 self.sa.commit()
194 self.sa.commit()
185 except:
195 except:
186 self.sa.rollback()
196 self.sa.rollback()
187 raise
197 raise
188
198
189 def fix_default_user(self):
199 def fix_default_user(self):
190 """
200 """
191 Fixes a old default user with some 'nicer' default values,
201 Fixes a old default user with some 'nicer' default values,
192 used mostly for anonymous access
202 used mostly for anonymous access
193 """
203 """
194 def_user = self.sa.query(User)\
204 def_user = self.sa.query(User)\
195 .filter(User.username == 'default')\
205 .filter(User.username == 'default')\
196 .one()
206 .one()
197
207
198 def_user.name = 'Anonymous'
208 def_user.name = 'Anonymous'
199 def_user.lastname = 'User'
209 def_user.lastname = 'User'
200 def_user.email = 'anonymous@rhodecode.org'
210 def_user.email = 'anonymous@rhodecode.org'
201
211
202 try:
212 try:
203 self.sa.add(def_user)
213 self.sa.add(def_user)
204 self.sa.commit()
214 self.sa.commit()
205 except:
215 except:
206 self.sa.rollback()
216 self.sa.rollback()
207 raise
217 raise
208
218
209 def fix_settings(self):
219 def fix_settings(self):
210 """
220 """
211 Fixes rhodecode settings adds ga_code key for google analytics
221 Fixes rhodecode settings adds ga_code key for google analytics
212 """
222 """
213
223
214 hgsettings3 = RhodeCodeSetting('ga_code', '')
224 hgsettings3 = RhodeCodeSetting('ga_code', '')
215
225
216 try:
226 try:
217 self.sa.add(hgsettings3)
227 self.sa.add(hgsettings3)
218 self.sa.commit()
228 self.sa.commit()
219 except:
229 except:
220 self.sa.rollback()
230 self.sa.rollback()
221 raise
231 raise
222
232
223 def admin_prompt(self, second=False):
233 def admin_prompt(self, second=False):
224 if not self.tests:
234 if not self.tests:
225 import getpass
235 import getpass
226
236
227 def get_password():
237 def get_password():
228 password = getpass.getpass('Specify admin password '
238 password = getpass.getpass('Specify admin password '
229 '(min 6 chars):')
239 '(min 6 chars):')
230 confirm = getpass.getpass('Confirm password:')
240 confirm = getpass.getpass('Confirm password:')
231
241
232 if password != confirm:
242 if password != confirm:
233 log.error('passwords mismatch')
243 log.error('passwords mismatch')
234 return False
244 return False
235 if len(password) < 6:
245 if len(password) < 6:
236 log.error('password is to short use at least 6 characters')
246 log.error('password is to short use at least 6 characters')
237 return False
247 return False
238
248
239 return password
249 return password
240
250
241 username = raw_input('Specify admin username:')
251 username = raw_input('Specify admin username:')
242
252
243 password = get_password()
253 password = get_password()
244 if not password:
254 if not password:
245 #second try
255 #second try
246 password = get_password()
256 password = get_password()
247 if not password:
257 if not password:
248 sys.exit()
258 sys.exit()
249
259
250 email = raw_input('Specify admin email:')
260 email = raw_input('Specify admin email:')
251 self.create_user(username, password, email, True)
261 self.create_user(username, password, email, True)
252 else:
262 else:
253 log.info('creating admin and regular test users')
263 log.info('creating admin and regular test users')
254 from rhodecode.tests import TEST_USER_ADMIN_LOGIN,\
264 from rhodecode.tests import TEST_USER_ADMIN_LOGIN,\
255 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL,\
265 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL,\
256 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,\
266 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,\
257 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
267 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
258 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
268 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
259
269
260 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
270 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
261 TEST_USER_ADMIN_EMAIL, True)
271 TEST_USER_ADMIN_EMAIL, True)
262
272
263 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
273 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
264 TEST_USER_REGULAR_EMAIL, False)
274 TEST_USER_REGULAR_EMAIL, False)
265
275
266 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
276 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
267 TEST_USER_REGULAR2_EMAIL, False)
277 TEST_USER_REGULAR2_EMAIL, False)
268
278
269 def create_ui_settings(self):
279 def create_ui_settings(self):
270 """
280 """
271 Creates ui settings, fills out hooks
281 Creates ui settings, fills out hooks
272 and disables dotencode
282 and disables dotencode
273 """
283 """
274
284
275 #HOOKS
285 #HOOKS
276 hooks1_key = RhodeCodeUi.HOOK_UPDATE
286 hooks1_key = RhodeCodeUi.HOOK_UPDATE
277 hooks1_ = self.sa.query(RhodeCodeUi)\
287 hooks1_ = self.sa.query(RhodeCodeUi)\
278 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
288 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
279
289
280 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
290 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
281 hooks1.ui_section = 'hooks'
291 hooks1.ui_section = 'hooks'
282 hooks1.ui_key = hooks1_key
292 hooks1.ui_key = hooks1_key
283 hooks1.ui_value = 'hg update >&2'
293 hooks1.ui_value = 'hg update >&2'
284 hooks1.ui_active = False
294 hooks1.ui_active = False
285
295
286 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
296 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
287 hooks2_ = self.sa.query(RhodeCodeUi)\
297 hooks2_ = self.sa.query(RhodeCodeUi)\
288 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
298 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
289
299
290 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
300 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
291 hooks2.ui_section = 'hooks'
301 hooks2.ui_section = 'hooks'
292 hooks2.ui_key = hooks2_key
302 hooks2.ui_key = hooks2_key
293 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
303 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
294
304
295 hooks3 = RhodeCodeUi()
305 hooks3 = RhodeCodeUi()
296 hooks3.ui_section = 'hooks'
306 hooks3.ui_section = 'hooks'
297 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
307 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
298 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
308 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
299
309
300 hooks4 = RhodeCodeUi()
310 hooks4 = RhodeCodeUi()
301 hooks4.ui_section = 'hooks'
311 hooks4.ui_section = 'hooks'
302 hooks4.ui_key = RhodeCodeUi.HOOK_PULL
312 hooks4.ui_key = RhodeCodeUi.HOOK_PULL
303 hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
313 hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
304
314
305 # For mercurial 1.7 set backward comapatibility with format
315 # For mercurial 1.7 set backward comapatibility with format
306 dotencode_disable = RhodeCodeUi()
316 dotencode_disable = RhodeCodeUi()
307 dotencode_disable.ui_section = 'format'
317 dotencode_disable.ui_section = 'format'
308 dotencode_disable.ui_key = 'dotencode'
318 dotencode_disable.ui_key = 'dotencode'
309 dotencode_disable.ui_value = 'false'
319 dotencode_disable.ui_value = 'false'
310
320
311 # enable largefiles
321 # enable largefiles
312 largefiles = RhodeCodeUi()
322 largefiles = RhodeCodeUi()
313 largefiles.ui_section = 'extensions'
323 largefiles.ui_section = 'extensions'
314 largefiles.ui_key = 'largefiles'
324 largefiles.ui_key = 'largefiles'
315 largefiles.ui_value = ''
325 largefiles.ui_value = ''
316
326
317 self.sa.add(hooks1)
327 self.sa.add(hooks1)
318 self.sa.add(hooks2)
328 self.sa.add(hooks2)
319 self.sa.add(hooks3)
329 self.sa.add(hooks3)
320 self.sa.add(hooks4)
330 self.sa.add(hooks4)
321 self.sa.add(largefiles)
331 self.sa.add(largefiles)
322
332
323 def create_ldap_options(self, skip_existing=False):
333 def create_ldap_options(self, skip_existing=False):
324 """Creates ldap settings"""
334 """Creates ldap settings"""
325
335
326 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
336 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
327 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
337 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
328 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
338 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
329 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
339 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
330 ('ldap_filter', ''), ('ldap_search_scope', ''),
340 ('ldap_filter', ''), ('ldap_search_scope', ''),
331 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
341 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
332 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
342 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
333
343
334 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
344 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
335 log.debug('Skipping option %s' % k)
345 log.debug('Skipping option %s' % k)
336 continue
346 continue
337 setting = RhodeCodeSetting(k, v)
347 setting = RhodeCodeSetting(k, v)
338 self.sa.add(setting)
348 self.sa.add(setting)
339
349
340 def config_prompt(self, test_repo_path='', retries=3):
350 def config_prompt(self, test_repo_path='', retries=3):
341 if retries == 3:
351 if retries == 3:
342 log.info('Setting up repositories config')
352 log.info('Setting up repositories config')
343
353
344 if not self.tests and not test_repo_path:
354 if not self.tests and not test_repo_path:
345 path = raw_input('Specify valid full path to your repositories'
355 path = raw_input('Specify valid full path to your repositories'
346 ' you can change this later in application settings:')
356 ' you can change this later in application settings:')
347 else:
357 else:
348 path = test_repo_path
358 path = test_repo_path
349 path_ok = True
359 path_ok = True
350
360
351 # check proper dir
361 # check proper dir
352 if not os.path.isdir(path):
362 if not os.path.isdir(path):
353 path_ok = False
363 path_ok = False
354 log.error('Given path %s is not a valid directory', path)
364 log.error('Given path %s is not a valid directory', path)
355
365
356 # check write access
366 # check write access
357 if not os.access(path, os.W_OK) and path_ok:
367 if not os.access(path, os.W_OK) and path_ok:
358 path_ok = False
368 path_ok = False
359 log.error('No write permission to given path %s', path)
369 log.error('No write permission to given path %s', path)
360
370
361 if retries == 0:
371 if retries == 0:
362 sys.exit('max retries reached')
372 sys.exit('max retries reached')
363 if path_ok is False:
373 if path_ok is False:
364 retries -= 1
374 retries -= 1
365 return self.config_prompt(test_repo_path, retries)
375 return self.config_prompt(test_repo_path, retries)
366
376
367 return path
377 return path
368
378
369 def create_settings(self, path):
379 def create_settings(self, path):
370
380
371 self.create_ui_settings()
381 self.create_ui_settings()
372
382
373 #HG UI OPTIONS
383 #HG UI OPTIONS
374 web1 = RhodeCodeUi()
384 web1 = RhodeCodeUi()
375 web1.ui_section = 'web'
385 web1.ui_section = 'web'
376 web1.ui_key = 'push_ssl'
386 web1.ui_key = 'push_ssl'
377 web1.ui_value = 'false'
387 web1.ui_value = 'false'
378
388
379 web2 = RhodeCodeUi()
389 web2 = RhodeCodeUi()
380 web2.ui_section = 'web'
390 web2.ui_section = 'web'
381 web2.ui_key = 'allow_archive'
391 web2.ui_key = 'allow_archive'
382 web2.ui_value = 'gz zip bz2'
392 web2.ui_value = 'gz zip bz2'
383
393
384 web3 = RhodeCodeUi()
394 web3 = RhodeCodeUi()
385 web3.ui_section = 'web'
395 web3.ui_section = 'web'
386 web3.ui_key = 'allow_push'
396 web3.ui_key = 'allow_push'
387 web3.ui_value = '*'
397 web3.ui_value = '*'
388
398
389 web4 = RhodeCodeUi()
399 web4 = RhodeCodeUi()
390 web4.ui_section = 'web'
400 web4.ui_section = 'web'
391 web4.ui_key = 'baseurl'
401 web4.ui_key = 'baseurl'
392 web4.ui_value = '/'
402 web4.ui_value = '/'
393
403
394 paths = RhodeCodeUi()
404 paths = RhodeCodeUi()
395 paths.ui_section = 'paths'
405 paths.ui_section = 'paths'
396 paths.ui_key = '/'
406 paths.ui_key = '/'
397 paths.ui_value = path
407 paths.ui_value = path
398
408
399 hgsettings1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
409 hgsettings1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
400 hgsettings2 = RhodeCodeSetting('title', 'RhodeCode')
410 hgsettings2 = RhodeCodeSetting('title', 'RhodeCode')
401 hgsettings3 = RhodeCodeSetting('ga_code', '')
411 hgsettings3 = RhodeCodeSetting('ga_code', '')
402
412
403 self.sa.add(web1)
413 self.sa.add(web1)
404 self.sa.add(web2)
414 self.sa.add(web2)
405 self.sa.add(web3)
415 self.sa.add(web3)
406 self.sa.add(web4)
416 self.sa.add(web4)
407 self.sa.add(paths)
417 self.sa.add(paths)
408 self.sa.add(hgsettings1)
418 self.sa.add(hgsettings1)
409 self.sa.add(hgsettings2)
419 self.sa.add(hgsettings2)
410 self.sa.add(hgsettings3)
420 self.sa.add(hgsettings3)
411
421
412 self.create_ldap_options()
422 self.create_ldap_options()
413
423
414 log.info('created ui config')
424 log.info('created ui config')
415
425
416 def create_user(self, username, password, email='', admin=False):
426 def create_user(self, username, password, email='', admin=False):
417 log.info('creating user %s', username)
427 log.info('creating user %s', username)
418 UserModel().create_or_update(username, password, email,
428 UserModel().create_or_update(username, password, email,
419 name='RhodeCode', lastname='Admin',
429 name='RhodeCode', lastname='Admin',
420 active=True, admin=admin)
430 active=True, admin=admin)
421
431
422 def create_default_user(self):
432 def create_default_user(self):
423 log.info('creating default user')
433 log.info('creating default user')
424 # create default user for handling default permissions.
434 # create default user for handling default permissions.
425 UserModel().create_or_update(username='default',
435 UserModel().create_or_update(username='default',
426 password=str(uuid.uuid1())[:8],
436 password=str(uuid.uuid1())[:8],
427 email='anonymous@rhodecode.org',
437 email='anonymous@rhodecode.org',
428 name='Anonymous', lastname='User')
438 name='Anonymous', lastname='User')
429
439
430 def create_permissions(self):
440 def create_permissions(self):
431 # module.(access|create|change|delete)_[name]
441 # module.(access|create|change|delete)_[name]
432 # module.(read|write|owner)
442 # module.(read|write|owner)
433 perms = [('repository.none', 'Repository no access'),
443 perms = [('repository.none', 'Repository no access'),
434 ('repository.read', 'Repository read access'),
444 ('repository.read', 'Repository read access'),
435 ('repository.write', 'Repository write access'),
445 ('repository.write', 'Repository write access'),
436 ('repository.admin', 'Repository admin access'),
446 ('repository.admin', 'Repository admin access'),
437 ('hg.admin', 'Hg Administrator'),
447 ('hg.admin', 'Hg Administrator'),
438 ('hg.create.repository', 'Repository create'),
448 ('hg.create.repository', 'Repository create'),
439 ('hg.create.none', 'Repository creation disabled'),
449 ('hg.create.none', 'Repository creation disabled'),
440 ('hg.register.none', 'Register disabled'),
450 ('hg.register.none', 'Register disabled'),
441 ('hg.register.manual_activate', 'Register new user with '
451 ('hg.register.manual_activate', 'Register new user with '
442 'RhodeCode without manual'
452 'RhodeCode without manual'
443 'activation'),
453 'activation'),
444
454
445 ('hg.register.auto_activate', 'Register new user with '
455 ('hg.register.auto_activate', 'Register new user with '
446 'RhodeCode without auto '
456 'RhodeCode without auto '
447 'activation'),
457 'activation'),
448 ]
458 ]
449
459
450 for p in perms:
460 for p in perms:
451 new_perm = Permission()
461 new_perm = Permission()
452 new_perm.permission_name = p[0]
462 new_perm.permission_name = p[0]
453 new_perm.permission_longname = p[1]
463 new_perm.permission_longname = p[1]
454 self.sa.add(new_perm)
464 self.sa.add(new_perm)
455
465
456 def populate_default_permissions(self):
466 def populate_default_permissions(self):
457 log.info('creating default user permissions')
467 log.info('creating default user permissions')
458
468
459 default_user = self.sa.query(User)\
469 default_user = self.sa.query(User)\
460 .filter(User.username == 'default').scalar()
470 .filter(User.username == 'default').scalar()
461
471
462 reg_perm = UserToPerm()
472 reg_perm = UserToPerm()
463 reg_perm.user = default_user
473 reg_perm.user = default_user
464 reg_perm.permission = self.sa.query(Permission)\
474 reg_perm.permission = self.sa.query(Permission)\
465 .filter(Permission.permission_name == 'hg.register.manual_activate')\
475 .filter(Permission.permission_name == 'hg.register.manual_activate')\
466 .scalar()
476 .scalar()
467
477
468 create_repo_perm = UserToPerm()
478 create_repo_perm = UserToPerm()
469 create_repo_perm.user = default_user
479 create_repo_perm.user = default_user
470 create_repo_perm.permission = self.sa.query(Permission)\
480 create_repo_perm.permission = self.sa.query(Permission)\
471 .filter(Permission.permission_name == 'hg.create.repository')\
481 .filter(Permission.permission_name == 'hg.create.repository')\
472 .scalar()
482 .scalar()
473
483
474 default_repo_perm = UserToPerm()
484 default_repo_perm = UserToPerm()
475 default_repo_perm.user = default_user
485 default_repo_perm.user = default_user
476 default_repo_perm.permission = self.sa.query(Permission)\
486 default_repo_perm.permission = self.sa.query(Permission)\
477 .filter(Permission.permission_name == 'repository.read')\
487 .filter(Permission.permission_name == 'repository.read')\
478 .scalar()
488 .scalar()
479
489
480 self.sa.add(reg_perm)
490 self.sa.add(reg_perm)
481 self.sa.add(create_repo_perm)
491 self.sa.add(create_repo_perm)
482 self.sa.add(default_repo_perm)
492 self.sa.add(default_repo_perm)
@@ -1,755 +1,792 b''
1 """Helper functions
1 """Helper functions
2
2
3 Consists of functions to typically be used within templates, but also
3 Consists of functions to typically be used within templates, but also
4 available to Controllers. This module is available to both as 'h'.
4 available to Controllers. This module is available to both as 'h'.
5 """
5 """
6 import random
6 import random
7 import hashlib
7 import hashlib
8 import StringIO
8 import StringIO
9 import urllib
9 import urllib
10 import math
10 import math
11 import logging
11
12
12 from datetime import datetime
13 from datetime import datetime
13 from pygments.formatters.html import HtmlFormatter
14 from pygments.formatters.html import HtmlFormatter
14 from pygments import highlight as code_highlight
15 from pygments import highlight as code_highlight
15 from pylons import url, request, config
16 from pylons import url, request, config
16 from pylons.i18n.translation import _, ungettext
17 from pylons.i18n.translation import _, ungettext
17 from hashlib import md5
18 from hashlib import md5
18
19
19 from webhelpers.html import literal, HTML, escape
20 from webhelpers.html import literal, HTML, escape
20 from webhelpers.html.tools import *
21 from webhelpers.html.tools import *
21 from webhelpers.html.builder import make_tag
22 from webhelpers.html.builder import make_tag
22 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
23 end_form, file, form, hidden, image, javascript_link, link_to, \
24 end_form, file, form, hidden, image, javascript_link, link_to, \
24 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
25 submit, text, password, textarea, title, ul, xml_declaration, radio
26 submit, text, password, textarea, title, ul, xml_declaration, radio
26 from webhelpers.html.tools import auto_link, button_to, highlight, \
27 from webhelpers.html.tools import auto_link, button_to, highlight, \
27 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
28 from webhelpers.number import format_byte_size, format_bit_size
29 from webhelpers.number import format_byte_size, format_bit_size
29 from webhelpers.pylonslib import Flash as _Flash
30 from webhelpers.pylonslib import Flash as _Flash
30 from webhelpers.pylonslib.secure_form import secure_form
31 from webhelpers.pylonslib.secure_form import secure_form
31 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
32 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
32 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
33 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
33 replace_whitespace, urlify, truncate, wrap_paragraphs
34 replace_whitespace, urlify, truncate, wrap_paragraphs
34 from webhelpers.date import time_ago_in_words
35 from webhelpers.date import time_ago_in_words
35 from webhelpers.paginate import Page
36 from webhelpers.paginate import Page
36 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
37 convert_boolean_attrs, NotGiven, _make_safe_id_component
38 convert_boolean_attrs, NotGiven, _make_safe_id_component
38
39
39 from rhodecode.lib.annotate import annotate_highlight
40 from rhodecode.lib.annotate import annotate_highlight
40 from rhodecode.lib.utils import repo_name_slug
41 from rhodecode.lib.utils import repo_name_slug
41 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
42 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
42 from rhodecode.lib.markup_renderer import MarkupRenderer
43 from rhodecode.lib.markup_renderer import MarkupRenderer
43
44
45 log = logging.getLogger(__name__)
46
44
47
45 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
48 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
46 """
49 """
47 Reset button
50 Reset button
48 """
51 """
49 _set_input_attrs(attrs, type, name, value)
52 _set_input_attrs(attrs, type, name, value)
50 _set_id_attr(attrs, id, name)
53 _set_id_attr(attrs, id, name)
51 convert_boolean_attrs(attrs, ["disabled"])
54 convert_boolean_attrs(attrs, ["disabled"])
52 return HTML.input(**attrs)
55 return HTML.input(**attrs)
53
56
54 reset = _reset
57 reset = _reset
55 safeid = _make_safe_id_component
58 safeid = _make_safe_id_component
56
59
57
60
58 def FID(raw_id, path):
61 def FID(raw_id, path):
59 """
62 """
60 Creates a uniqe ID for filenode based on it's hash of path and revision
63 Creates a uniqe ID for filenode based on it's hash of path and revision
61 it's safe to use in urls
64 it's safe to use in urls
62
65
63 :param raw_id:
66 :param raw_id:
64 :param path:
67 :param path:
65 """
68 """
66
69
67 return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
70 return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
68
71
69
72
70 def get_token():
73 def get_token():
71 """Return the current authentication token, creating one if one doesn't
74 """Return the current authentication token, creating one if one doesn't
72 already exist.
75 already exist.
73 """
76 """
74 token_key = "_authentication_token"
77 token_key = "_authentication_token"
75 from pylons import session
78 from pylons import session
76 if not token_key in session:
79 if not token_key in session:
77 try:
80 try:
78 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
81 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
79 except AttributeError: # Python < 2.4
82 except AttributeError: # Python < 2.4
80 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
83 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
81 session[token_key] = token
84 session[token_key] = token
82 if hasattr(session, 'save'):
85 if hasattr(session, 'save'):
83 session.save()
86 session.save()
84 return session[token_key]
87 return session[token_key]
85
88
86 class _GetError(object):
89 class _GetError(object):
87 """Get error from form_errors, and represent it as span wrapped error
90 """Get error from form_errors, and represent it as span wrapped error
88 message
91 message
89
92
90 :param field_name: field to fetch errors for
93 :param field_name: field to fetch errors for
91 :param form_errors: form errors dict
94 :param form_errors: form errors dict
92 """
95 """
93
96
94 def __call__(self, field_name, form_errors):
97 def __call__(self, field_name, form_errors):
95 tmpl = """<span class="error_msg">%s</span>"""
98 tmpl = """<span class="error_msg">%s</span>"""
96 if form_errors and form_errors.has_key(field_name):
99 if form_errors and form_errors.has_key(field_name):
97 return literal(tmpl % form_errors.get(field_name))
100 return literal(tmpl % form_errors.get(field_name))
98
101
99 get_error = _GetError()
102 get_error = _GetError()
100
103
101 class _ToolTip(object):
104 class _ToolTip(object):
102
105
103 def __call__(self, tooltip_title, trim_at=50):
106 def __call__(self, tooltip_title, trim_at=50):
104 """Special function just to wrap our text into nice formatted
107 """Special function just to wrap our text into nice formatted
105 autowrapped text
108 autowrapped text
106
109
107 :param tooltip_title:
110 :param tooltip_title:
108 """
111 """
109 return escape(tooltip_title)
112 return escape(tooltip_title)
110 tooltip = _ToolTip()
113 tooltip = _ToolTip()
111
114
112 class _FilesBreadCrumbs(object):
115 class _FilesBreadCrumbs(object):
113
116
114 def __call__(self, repo_name, rev, paths):
117 def __call__(self, repo_name, rev, paths):
115 if isinstance(paths, str):
118 if isinstance(paths, str):
116 paths = safe_unicode(paths)
119 paths = safe_unicode(paths)
117 url_l = [link_to(repo_name, url('files_home',
120 url_l = [link_to(repo_name, url('files_home',
118 repo_name=repo_name,
121 repo_name=repo_name,
119 revision=rev, f_path=''))]
122 revision=rev, f_path=''))]
120 paths_l = paths.split('/')
123 paths_l = paths.split('/')
121 for cnt, p in enumerate(paths_l):
124 for cnt, p in enumerate(paths_l):
122 if p != '':
125 if p != '':
123 url_l.append(link_to(p,
126 url_l.append(link_to(p,
124 url('files_home',
127 url('files_home',
125 repo_name=repo_name,
128 repo_name=repo_name,
126 revision=rev,
129 revision=rev,
127 f_path='/'.join(paths_l[:cnt + 1])
130 f_path='/'.join(paths_l[:cnt + 1])
128 )
131 )
129 )
132 )
130 )
133 )
131
134
132 return literal('/'.join(url_l))
135 return literal('/'.join(url_l))
133
136
134 files_breadcrumbs = _FilesBreadCrumbs()
137 files_breadcrumbs = _FilesBreadCrumbs()
135
138
136 class CodeHtmlFormatter(HtmlFormatter):
139 class CodeHtmlFormatter(HtmlFormatter):
137 """My code Html Formatter for source codes
140 """My code Html Formatter for source codes
138 """
141 """
139
142
140 def wrap(self, source, outfile):
143 def wrap(self, source, outfile):
141 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
144 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
142
145
143 def _wrap_code(self, source):
146 def _wrap_code(self, source):
144 for cnt, it in enumerate(source):
147 for cnt, it in enumerate(source):
145 i, t = it
148 i, t = it
146 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
149 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
147 yield i, t
150 yield i, t
148
151
149 def _wrap_tablelinenos(self, inner):
152 def _wrap_tablelinenos(self, inner):
150 dummyoutfile = StringIO.StringIO()
153 dummyoutfile = StringIO.StringIO()
151 lncount = 0
154 lncount = 0
152 for t, line in inner:
155 for t, line in inner:
153 if t:
156 if t:
154 lncount += 1
157 lncount += 1
155 dummyoutfile.write(line)
158 dummyoutfile.write(line)
156
159
157 fl = self.linenostart
160 fl = self.linenostart
158 mw = len(str(lncount + fl - 1))
161 mw = len(str(lncount + fl - 1))
159 sp = self.linenospecial
162 sp = self.linenospecial
160 st = self.linenostep
163 st = self.linenostep
161 la = self.lineanchors
164 la = self.lineanchors
162 aln = self.anchorlinenos
165 aln = self.anchorlinenos
163 nocls = self.noclasses
166 nocls = self.noclasses
164 if sp:
167 if sp:
165 lines = []
168 lines = []
166
169
167 for i in range(fl, fl + lncount):
170 for i in range(fl, fl + lncount):
168 if i % st == 0:
171 if i % st == 0:
169 if i % sp == 0:
172 if i % sp == 0:
170 if aln:
173 if aln:
171 lines.append('<a href="#%s%d" class="special">%*d</a>' %
174 lines.append('<a href="#%s%d" class="special">%*d</a>' %
172 (la, i, mw, i))
175 (la, i, mw, i))
173 else:
176 else:
174 lines.append('<span class="special">%*d</span>' % (mw, i))
177 lines.append('<span class="special">%*d</span>' % (mw, i))
175 else:
178 else:
176 if aln:
179 if aln:
177 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
180 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
178 else:
181 else:
179 lines.append('%*d' % (mw, i))
182 lines.append('%*d' % (mw, i))
180 else:
183 else:
181 lines.append('')
184 lines.append('')
182 ls = '\n'.join(lines)
185 ls = '\n'.join(lines)
183 else:
186 else:
184 lines = []
187 lines = []
185 for i in range(fl, fl + lncount):
188 for i in range(fl, fl + lncount):
186 if i % st == 0:
189 if i % st == 0:
187 if aln:
190 if aln:
188 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
191 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
189 else:
192 else:
190 lines.append('%*d' % (mw, i))
193 lines.append('%*d' % (mw, i))
191 else:
194 else:
192 lines.append('')
195 lines.append('')
193 ls = '\n'.join(lines)
196 ls = '\n'.join(lines)
194
197
195 # in case you wonder about the seemingly redundant <div> here: since the
198 # in case you wonder about the seemingly redundant <div> here: since the
196 # content in the other cell also is wrapped in a div, some browsers in
199 # content in the other cell also is wrapped in a div, some browsers in
197 # some configurations seem to mess up the formatting...
200 # some configurations seem to mess up the formatting...
198 if nocls:
201 if nocls:
199 yield 0, ('<table class="%stable">' % self.cssclass +
202 yield 0, ('<table class="%stable">' % self.cssclass +
200 '<tr><td><div class="linenodiv" '
203 '<tr><td><div class="linenodiv" '
201 'style="background-color: #f0f0f0; padding-right: 10px">'
204 'style="background-color: #f0f0f0; padding-right: 10px">'
202 '<pre style="line-height: 125%">' +
205 '<pre style="line-height: 125%">' +
203 ls + '</pre></div></td><td id="hlcode" class="code">')
206 ls + '</pre></div></td><td id="hlcode" class="code">')
204 else:
207 else:
205 yield 0, ('<table class="%stable">' % self.cssclass +
208 yield 0, ('<table class="%stable">' % self.cssclass +
206 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
209 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
207 ls + '</pre></div></td><td id="hlcode" class="code">')
210 ls + '</pre></div></td><td id="hlcode" class="code">')
208 yield 0, dummyoutfile.getvalue()
211 yield 0, dummyoutfile.getvalue()
209 yield 0, '</td></tr></table>'
212 yield 0, '</td></tr></table>'
210
213
211
214
212 def pygmentize(filenode, **kwargs):
215 def pygmentize(filenode, **kwargs):
213 """pygmentize function using pygments
216 """pygmentize function using pygments
214
217
215 :param filenode:
218 :param filenode:
216 """
219 """
217
220
218 return literal(code_highlight(filenode.content,
221 return literal(code_highlight(filenode.content,
219 filenode.lexer, CodeHtmlFormatter(**kwargs)))
222 filenode.lexer, CodeHtmlFormatter(**kwargs)))
220
223
221
224
222 def pygmentize_annotation(repo_name, filenode, **kwargs):
225 def pygmentize_annotation(repo_name, filenode, **kwargs):
223 """
226 """
224 pygmentize function for annotation
227 pygmentize function for annotation
225
228
226 :param filenode:
229 :param filenode:
227 """
230 """
228
231
229 color_dict = {}
232 color_dict = {}
230
233
231 def gen_color(n=10000):
234 def gen_color(n=10000):
232 """generator for getting n of evenly distributed colors using
235 """generator for getting n of evenly distributed colors using
233 hsv color and golden ratio. It always return same order of colors
236 hsv color and golden ratio. It always return same order of colors
234
237
235 :returns: RGB tuple
238 :returns: RGB tuple
236 """
239 """
237
240
238 def hsv_to_rgb(h, s, v):
241 def hsv_to_rgb(h, s, v):
239 if s == 0.0:
242 if s == 0.0:
240 return v, v, v
243 return v, v, v
241 i = int(h * 6.0) # XXX assume int() truncates!
244 i = int(h * 6.0) # XXX assume int() truncates!
242 f = (h * 6.0) - i
245 f = (h * 6.0) - i
243 p = v * (1.0 - s)
246 p = v * (1.0 - s)
244 q = v * (1.0 - s * f)
247 q = v * (1.0 - s * f)
245 t = v * (1.0 - s * (1.0 - f))
248 t = v * (1.0 - s * (1.0 - f))
246 i = i % 6
249 i = i % 6
247 if i == 0:
250 if i == 0:
248 return v, t, p
251 return v, t, p
249 if i == 1:
252 if i == 1:
250 return q, v, p
253 return q, v, p
251 if i == 2:
254 if i == 2:
252 return p, v, t
255 return p, v, t
253 if i == 3:
256 if i == 3:
254 return p, q, v
257 return p, q, v
255 if i == 4:
258 if i == 4:
256 return t, p, v
259 return t, p, v
257 if i == 5:
260 if i == 5:
258 return v, p, q
261 return v, p, q
259
262
260 golden_ratio = 0.618033988749895
263 golden_ratio = 0.618033988749895
261 h = 0.22717784590367374
264 h = 0.22717784590367374
262
265
263 for _ in xrange(n):
266 for _ in xrange(n):
264 h += golden_ratio
267 h += golden_ratio
265 h %= 1
268 h %= 1
266 HSV_tuple = [h, 0.95, 0.95]
269 HSV_tuple = [h, 0.95, 0.95]
267 RGB_tuple = hsv_to_rgb(*HSV_tuple)
270 RGB_tuple = hsv_to_rgb(*HSV_tuple)
268 yield map(lambda x: str(int(x * 256)), RGB_tuple)
271 yield map(lambda x: str(int(x * 256)), RGB_tuple)
269
272
270 cgenerator = gen_color()
273 cgenerator = gen_color()
271
274
272 def get_color_string(cs):
275 def get_color_string(cs):
273 if cs in color_dict:
276 if cs in color_dict:
274 col = color_dict[cs]
277 col = color_dict[cs]
275 else:
278 else:
276 col = color_dict[cs] = cgenerator.next()
279 col = color_dict[cs] = cgenerator.next()
277 return "color: rgb(%s)! important;" % (', '.join(col))
280 return "color: rgb(%s)! important;" % (', '.join(col))
278
281
279 def url_func(repo_name):
282 def url_func(repo_name):
280
283
281 def _url_func(changeset):
284 def _url_func(changeset):
282 author = changeset.author
285 author = changeset.author
283 date = changeset.date
286 date = changeset.date
284 message = tooltip(changeset.message)
287 message = tooltip(changeset.message)
285
288
286 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
289 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
287 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
290 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
288 "</b> %s<br/></div>")
291 "</b> %s<br/></div>")
289
292
290 tooltip_html = tooltip_html % (author, date, message)
293 tooltip_html = tooltip_html % (author, date, message)
291 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
294 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
292 short_id(changeset.raw_id))
295 short_id(changeset.raw_id))
293 uri = link_to(
296 uri = link_to(
294 lnk_format,
297 lnk_format,
295 url('changeset_home', repo_name=repo_name,
298 url('changeset_home', repo_name=repo_name,
296 revision=changeset.raw_id),
299 revision=changeset.raw_id),
297 style=get_color_string(changeset.raw_id),
300 style=get_color_string(changeset.raw_id),
298 class_='tooltip',
301 class_='tooltip',
299 title=tooltip_html
302 title=tooltip_html
300 )
303 )
301
304
302 uri += '\n'
305 uri += '\n'
303 return uri
306 return uri
304 return _url_func
307 return _url_func
305
308
306 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
309 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
307
310
308
311
309 def is_following_repo(repo_name, user_id):
312 def is_following_repo(repo_name, user_id):
310 from rhodecode.model.scm import ScmModel
313 from rhodecode.model.scm import ScmModel
311 return ScmModel().is_following_repo(repo_name, user_id)
314 return ScmModel().is_following_repo(repo_name, user_id)
312
315
313 flash = _Flash()
316 flash = _Flash()
314
317
315 #==============================================================================
318 #==============================================================================
316 # SCM FILTERS available via h.
319 # SCM FILTERS available via h.
317 #==============================================================================
320 #==============================================================================
318 from vcs.utils import author_name, author_email
321 from vcs.utils import author_name, author_email
319 from rhodecode.lib import credentials_filter, age as _age
322 from rhodecode.lib import credentials_filter, age as _age
320 from rhodecode.model.db import User
323 from rhodecode.model.db import User
321
324
322 age = lambda x: _age(x)
325 age = lambda x: _age(x)
323 capitalize = lambda x: x.capitalize()
326 capitalize = lambda x: x.capitalize()
324 email = author_email
327 email = author_email
325 short_id = lambda x: x[:12]
328 short_id = lambda x: x[:12]
326 hide_credentials = lambda x: ''.join(credentials_filter(x))
329 hide_credentials = lambda x: ''.join(credentials_filter(x))
327
330
328
331
329 def email_or_none(author):
332 def email_or_none(author):
330 _email = email(author)
333 _email = email(author)
331 if _email != '':
334 if _email != '':
332 return _email
335 return _email
333
336
334 # See if it contains a username we can get an email from
337 # See if it contains a username we can get an email from
335 user = User.get_by_username(author_name(author), case_insensitive=True,
338 user = User.get_by_username(author_name(author), case_insensitive=True,
336 cache=True)
339 cache=True)
337 if user is not None:
340 if user is not None:
338 return user.email
341 return user.email
339
342
340 # No valid email, not a valid user in the system, none!
343 # No valid email, not a valid user in the system, none!
341 return None
344 return None
342
345
343
346
344 def person(author):
347 def person(author):
345 # attr to return from fetched user
348 # attr to return from fetched user
346 person_getter = lambda usr: usr.username
349 person_getter = lambda usr: usr.username
347
350
348 # Valid email in the attribute passed, see if they're in the system
351 # Valid email in the attribute passed, see if they're in the system
349 _email = email(author)
352 _email = email(author)
350 if _email != '':
353 if _email != '':
351 user = User.get_by_email(_email, case_insensitive=True, cache=True)
354 user = User.get_by_email(_email, case_insensitive=True, cache=True)
352 if user is not None:
355 if user is not None:
353 return person_getter(user)
356 return person_getter(user)
354 return _email
357 return _email
355
358
356 # Maybe it's a username?
359 # Maybe it's a username?
357 _author = author_name(author)
360 _author = author_name(author)
358 user = User.get_by_username(_author, case_insensitive=True,
361 user = User.get_by_username(_author, case_insensitive=True,
359 cache=True)
362 cache=True)
360 if user is not None:
363 if user is not None:
361 return person_getter(user)
364 return person_getter(user)
362
365
363 # Still nothing? Just pass back the author name then
366 # Still nothing? Just pass back the author name then
364 return _author
367 return _author
365
368
366 def bool2icon(value):
369 def bool2icon(value):
367 """Returns True/False values represented as small html image of true/false
370 """Returns True/False values represented as small html image of true/false
368 icons
371 icons
369
372
370 :param value: bool value
373 :param value: bool value
371 """
374 """
372
375
373 if value is True:
376 if value is True:
374 return HTML.tag('img', src=url("/images/icons/accept.png"),
377 return HTML.tag('img', src=url("/images/icons/accept.png"),
375 alt=_('True'))
378 alt=_('True'))
376
379
377 if value is False:
380 if value is False:
378 return HTML.tag('img', src=url("/images/icons/cancel.png"),
381 return HTML.tag('img', src=url("/images/icons/cancel.png"),
379 alt=_('False'))
382 alt=_('False'))
380
383
381 return value
384 return value
382
385
383
386
384 def action_parser(user_log, feed=False):
387 def action_parser(user_log, feed=False):
385 """This helper will action_map the specified string action into translated
388 """This helper will action_map the specified string action into translated
386 fancy names with icons and links
389 fancy names with icons and links
387
390
388 :param user_log: user log instance
391 :param user_log: user log instance
389 :param feed: use output for feeds (no html and fancy icons)
392 :param feed: use output for feeds (no html and fancy icons)
390 """
393 """
391
394
392 action = user_log.action
395 action = user_log.action
393 action_params = ' '
396 action_params = ' '
394
397
395 x = action.split(':')
398 x = action.split(':')
396
399
397 if len(x) > 1:
400 if len(x) > 1:
398 action, action_params = x
401 action, action_params = x
399
402
400 def get_cs_links():
403 def get_cs_links():
401 revs_limit = 3 #display this amount always
404 revs_limit = 3 #display this amount always
402 revs_top_limit = 50 #show upto this amount of changesets hidden
405 revs_top_limit = 50 #show upto this amount of changesets hidden
403 revs = action_params.split(',')
406 revs = action_params.split(',')
404 repo_name = user_log.repository.repo_name
407 repo_name = user_log.repository.repo_name
405
408
406 from rhodecode.model.scm import ScmModel
409 from rhodecode.model.scm import ScmModel
407 repo = user_log.repository.scm_instance
410 repo = user_log.repository.scm_instance
408
411
409 message = lambda rev: get_changeset_safe(repo, rev).message
412 message = lambda rev: get_changeset_safe(repo, rev).message
410 cs_links = []
413 cs_links = []
411 cs_links.append(" " + ', '.join ([link_to(rev,
414 cs_links.append(" " + ', '.join ([link_to(rev,
412 url('changeset_home',
415 url('changeset_home',
413 repo_name=repo_name,
416 repo_name=repo_name,
414 revision=rev), title=tooltip(message(rev)),
417 revision=rev), title=tooltip(message(rev)),
415 class_='tooltip') for rev in revs[:revs_limit] ]))
418 class_='tooltip') for rev in revs[:revs_limit] ]))
416
419
417 compare_view = (' <div class="compare_view tooltip" title="%s">'
420 compare_view = (' <div class="compare_view tooltip" title="%s">'
418 '<a href="%s">%s</a> '
421 '<a href="%s">%s</a> '
419 '</div>' % (_('Show all combined changesets %s->%s' \
422 '</div>' % (_('Show all combined changesets %s->%s' \
420 % (revs[0], revs[-1])),
423 % (revs[0], revs[-1])),
421 url('changeset_home', repo_name=repo_name,
424 url('changeset_home', repo_name=repo_name,
422 revision='%s...%s' % (revs[0], revs[-1])
425 revision='%s...%s' % (revs[0], revs[-1])
423 ),
426 ),
424 _('compare view'))
427 _('compare view'))
425 )
428 )
426
429
427 if len(revs) > revs_limit:
430 if len(revs) > revs_limit:
428 uniq_id = revs[0]
431 uniq_id = revs[0]
429 html_tmpl = ('<span> %s '
432 html_tmpl = ('<span> %s '
430 '<a class="show_more" id="_%s" href="#more">%s</a> '
433 '<a class="show_more" id="_%s" href="#more">%s</a> '
431 '%s</span>')
434 '%s</span>')
432 if not feed:
435 if not feed:
433 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
436 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
434 % (len(revs) - revs_limit),
437 % (len(revs) - revs_limit),
435 _('revisions')))
438 _('revisions')))
436
439
437 if not feed:
440 if not feed:
438 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
441 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
439 else:
442 else:
440 html_tmpl = '<span id="%s"> %s </span>'
443 html_tmpl = '<span id="%s"> %s </span>'
441
444
442 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
445 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
443 url('changeset_home',
446 url('changeset_home',
444 repo_name=repo_name, revision=rev),
447 repo_name=repo_name, revision=rev),
445 title=message(rev), class_='tooltip')
448 title=message(rev), class_='tooltip')
446 for rev in revs[revs_limit:revs_top_limit]])))
449 for rev in revs[revs_limit:revs_top_limit]])))
447 if len(revs) > 1:
450 if len(revs) > 1:
448 cs_links.append(compare_view)
451 cs_links.append(compare_view)
449 return ''.join(cs_links)
452 return ''.join(cs_links)
450
453
451 def get_fork_name():
454 def get_fork_name():
452 repo_name = action_params
455 repo_name = action_params
453 return _('fork name ') + str(link_to(action_params, url('summary_home',
456 return _('fork name ') + str(link_to(action_params, url('summary_home',
454 repo_name=repo_name,)))
457 repo_name=repo_name,)))
455
458
456 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
459 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
457 'user_created_repo':(_('[created] repository'), None),
460 'user_created_repo':(_('[created] repository'), None),
458 'user_created_fork':(_('[created] repository as fork'), None),
461 'user_created_fork':(_('[created] repository as fork'), None),
459 'user_forked_repo':(_('[forked] repository'), get_fork_name),
462 'user_forked_repo':(_('[forked] repository'), get_fork_name),
460 'user_updated_repo':(_('[updated] repository'), None),
463 'user_updated_repo':(_('[updated] repository'), None),
461 'admin_deleted_repo':(_('[delete] repository'), None),
464 'admin_deleted_repo':(_('[delete] repository'), None),
462 'admin_created_repo':(_('[created] repository'), None),
465 'admin_created_repo':(_('[created] repository'), None),
463 'admin_forked_repo':(_('[forked] repository'), None),
466 'admin_forked_repo':(_('[forked] repository'), None),
464 'admin_updated_repo':(_('[updated] repository'), None),
467 'admin_updated_repo':(_('[updated] repository'), None),
465 'push':(_('[pushed] into'), get_cs_links),
468 'push':(_('[pushed] into'), get_cs_links),
466 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
469 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
467 'push_remote':(_('[pulled from remote] into'), get_cs_links),
470 'push_remote':(_('[pulled from remote] into'), get_cs_links),
468 'pull':(_('[pulled] from'), None),
471 'pull':(_('[pulled] from'), None),
469 'started_following_repo':(_('[started following] repository'), None),
472 'started_following_repo':(_('[started following] repository'), None),
470 'stopped_following_repo':(_('[stopped following] repository'), None),
473 'stopped_following_repo':(_('[stopped following] repository'), None),
471 }
474 }
472
475
473 action_str = action_map.get(action, action)
476 action_str = action_map.get(action, action)
474 if feed:
477 if feed:
475 action = action_str[0].replace('[', '').replace(']', '')
478 action = action_str[0].replace('[', '').replace(']', '')
476 else:
479 else:
477 action = action_str[0].replace('[', '<span class="journal_highlight">')\
480 action = action_str[0].replace('[', '<span class="journal_highlight">')\
478 .replace(']', '</span>')
481 .replace(']', '</span>')
479
482
480 action_params_func = lambda :""
483 action_params_func = lambda :""
481
484
482 if callable(action_str[1]):
485 if callable(action_str[1]):
483 action_params_func = action_str[1]
486 action_params_func = action_str[1]
484
487
485 return [literal(action), action_params_func]
488 return [literal(action), action_params_func]
486
489
487 def action_parser_icon(user_log):
490 def action_parser_icon(user_log):
488 action = user_log.action
491 action = user_log.action
489 action_params = None
492 action_params = None
490 x = action.split(':')
493 x = action.split(':')
491
494
492 if len(x) > 1:
495 if len(x) > 1:
493 action, action_params = x
496 action, action_params = x
494
497
495 tmpl = """<img src="%s%s" alt="%s"/>"""
498 tmpl = """<img src="%s%s" alt="%s"/>"""
496 map = {'user_deleted_repo':'database_delete.png',
499 map = {'user_deleted_repo':'database_delete.png',
497 'user_created_repo':'database_add.png',
500 'user_created_repo':'database_add.png',
498 'user_created_fork':'arrow_divide.png',
501 'user_created_fork':'arrow_divide.png',
499 'user_forked_repo':'arrow_divide.png',
502 'user_forked_repo':'arrow_divide.png',
500 'user_updated_repo':'database_edit.png',
503 'user_updated_repo':'database_edit.png',
501 'admin_deleted_repo':'database_delete.png',
504 'admin_deleted_repo':'database_delete.png',
502 'admin_created_repo':'database_add.png',
505 'admin_created_repo':'database_add.png',
503 'admin_forked_repo':'arrow_divide.png',
506 'admin_forked_repo':'arrow_divide.png',
504 'admin_updated_repo':'database_edit.png',
507 'admin_updated_repo':'database_edit.png',
505 'push':'script_add.png',
508 'push':'script_add.png',
506 'push_local':'script_edit.png',
509 'push_local':'script_edit.png',
507 'push_remote':'connect.png',
510 'push_remote':'connect.png',
508 'pull':'down_16.png',
511 'pull':'down_16.png',
509 'started_following_repo':'heart_add.png',
512 'started_following_repo':'heart_add.png',
510 'stopped_following_repo':'heart_delete.png',
513 'stopped_following_repo':'heart_delete.png',
511 }
514 }
512 return literal(tmpl % ((url('/images/icons/')),
515 return literal(tmpl % ((url('/images/icons/')),
513 map.get(action, action), action))
516 map.get(action, action), action))
514
517
515
518
516 #==============================================================================
519 #==============================================================================
517 # PERMS
520 # PERMS
518 #==============================================================================
521 #==============================================================================
519 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
522 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
520 HasRepoPermissionAny, HasRepoPermissionAll
523 HasRepoPermissionAny, HasRepoPermissionAll
521
524
522 #==============================================================================
525 #==============================================================================
523 # GRAVATAR URL
526 # GRAVATAR URL
524 #==============================================================================
527 #==============================================================================
525
528
526 def gravatar_url(email_address, size=30):
529 def gravatar_url(email_address, size=30):
527 if (not str2bool(config['app_conf'].get('use_gravatar')) or
530 if (not str2bool(config['app_conf'].get('use_gravatar')) or
528 not email_address or email_address == 'anonymous@rhodecode.org'):
531 not email_address or email_address == 'anonymous@rhodecode.org'):
529 return url("/images/user%s.png" % size)
532 return url("/images/user%s.png" % size)
530
533
531 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
534 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
532 default = 'identicon'
535 default = 'identicon'
533 baseurl_nossl = "http://www.gravatar.com/avatar/"
536 baseurl_nossl = "http://www.gravatar.com/avatar/"
534 baseurl_ssl = "https://secure.gravatar.com/avatar/"
537 baseurl_ssl = "https://secure.gravatar.com/avatar/"
535 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
538 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
536
539
537 if isinstance(email_address, unicode):
540 if isinstance(email_address, unicode):
538 #hashlib crashes on unicode items
541 #hashlib crashes on unicode items
539 email_address = safe_str(email_address)
542 email_address = safe_str(email_address)
540 # construct the url
543 # construct the url
541 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
544 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
542 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
545 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
543
546
544 return gravatar_url
547 return gravatar_url
545
548
546
549
547 #==============================================================================
550 #==============================================================================
548 # REPO PAGER, PAGER FOR REPOSITORY
551 # REPO PAGER, PAGER FOR REPOSITORY
549 #==============================================================================
552 #==============================================================================
550 class RepoPage(Page):
553 class RepoPage(Page):
551
554
552 def __init__(self, collection, page=1, items_per_page=20,
555 def __init__(self, collection, page=1, items_per_page=20,
553 item_count=None, url=None, **kwargs):
556 item_count=None, url=None, **kwargs):
554
557
555 """Create a "RepoPage" instance. special pager for paging
558 """Create a "RepoPage" instance. special pager for paging
556 repository
559 repository
557 """
560 """
558 self._url_generator = url
561 self._url_generator = url
559
562
560 # Safe the kwargs class-wide so they can be used in the pager() method
563 # Safe the kwargs class-wide so they can be used in the pager() method
561 self.kwargs = kwargs
564 self.kwargs = kwargs
562
565
563 # Save a reference to the collection
566 # Save a reference to the collection
564 self.original_collection = collection
567 self.original_collection = collection
565
568
566 self.collection = collection
569 self.collection = collection
567
570
568 # The self.page is the number of the current page.
571 # The self.page is the number of the current page.
569 # The first page has the number 1!
572 # The first page has the number 1!
570 try:
573 try:
571 self.page = int(page) # make it int() if we get it as a string
574 self.page = int(page) # make it int() if we get it as a string
572 except (ValueError, TypeError):
575 except (ValueError, TypeError):
573 self.page = 1
576 self.page = 1
574
577
575 self.items_per_page = items_per_page
578 self.items_per_page = items_per_page
576
579
577 # Unless the user tells us how many items the collections has
580 # Unless the user tells us how many items the collections has
578 # we calculate that ourselves.
581 # we calculate that ourselves.
579 if item_count is not None:
582 if item_count is not None:
580 self.item_count = item_count
583 self.item_count = item_count
581 else:
584 else:
582 self.item_count = len(self.collection)
585 self.item_count = len(self.collection)
583
586
584 # Compute the number of the first and last available page
587 # Compute the number of the first and last available page
585 if self.item_count > 0:
588 if self.item_count > 0:
586 self.first_page = 1
589 self.first_page = 1
587 self.page_count = int(math.ceil(float(self.item_count) /
590 self.page_count = int(math.ceil(float(self.item_count) /
588 self.items_per_page))
591 self.items_per_page))
589 self.last_page = self.first_page + self.page_count - 1
592 self.last_page = self.first_page + self.page_count - 1
590
593
591 # Make sure that the requested page number is the range of
594 # Make sure that the requested page number is the range of
592 # valid pages
595 # valid pages
593 if self.page > self.last_page:
596 if self.page > self.last_page:
594 self.page = self.last_page
597 self.page = self.last_page
595 elif self.page < self.first_page:
598 elif self.page < self.first_page:
596 self.page = self.first_page
599 self.page = self.first_page
597
600
598 # Note: the number of items on this page can be less than
601 # Note: the number of items on this page can be less than
599 # items_per_page if the last page is not full
602 # items_per_page if the last page is not full
600 self.first_item = max(0, (self.item_count) - (self.page *
603 self.first_item = max(0, (self.item_count) - (self.page *
601 items_per_page))
604 items_per_page))
602 self.last_item = ((self.item_count - 1) - items_per_page *
605 self.last_item = ((self.item_count - 1) - items_per_page *
603 (self.page - 1))
606 (self.page - 1))
604
607
605 self.items = list(self.collection[self.first_item:self.last_item + 1])
608 self.items = list(self.collection[self.first_item:self.last_item + 1])
606
609
607
610
608 # Links to previous and next page
611 # Links to previous and next page
609 if self.page > self.first_page:
612 if self.page > self.first_page:
610 self.previous_page = self.page - 1
613 self.previous_page = self.page - 1
611 else:
614 else:
612 self.previous_page = None
615 self.previous_page = None
613
616
614 if self.page < self.last_page:
617 if self.page < self.last_page:
615 self.next_page = self.page + 1
618 self.next_page = self.page + 1
616 else:
619 else:
617 self.next_page = None
620 self.next_page = None
618
621
619 # No items available
622 # No items available
620 else:
623 else:
621 self.first_page = None
624 self.first_page = None
622 self.page_count = 0
625 self.page_count = 0
623 self.last_page = None
626 self.last_page = None
624 self.first_item = None
627 self.first_item = None
625 self.last_item = None
628 self.last_item = None
626 self.previous_page = None
629 self.previous_page = None
627 self.next_page = None
630 self.next_page = None
628 self.items = []
631 self.items = []
629
632
630 # This is a subclass of the 'list' type. Initialise the list now.
633 # This is a subclass of the 'list' type. Initialise the list now.
631 list.__init__(self, reversed(self.items))
634 list.__init__(self, reversed(self.items))
632
635
633
636
634 def changed_tooltip(nodes):
637 def changed_tooltip(nodes):
635 """
638 """
636 Generates a html string for changed nodes in changeset page.
639 Generates a html string for changed nodes in changeset page.
637 It limits the output to 30 entries
640 It limits the output to 30 entries
638
641
639 :param nodes: LazyNodesGenerator
642 :param nodes: LazyNodesGenerator
640 """
643 """
641 if nodes:
644 if nodes:
642 pref = ': <br/> '
645 pref = ': <br/> '
643 suf = ''
646 suf = ''
644 if len(nodes) > 30:
647 if len(nodes) > 30:
645 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
648 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
646 return literal(pref + '<br/> '.join([safe_unicode(x.path)
649 return literal(pref + '<br/> '.join([safe_unicode(x.path)
647 for x in nodes[:30]]) + suf)
650 for x in nodes[:30]]) + suf)
648 else:
651 else:
649 return ': ' + _('No Files')
652 return ': ' + _('No Files')
650
653
651
654
652
655
653 def repo_link(groups_and_repos):
656 def repo_link(groups_and_repos):
654 """
657 """
655 Makes a breadcrumbs link to repo within a group
658 Makes a breadcrumbs link to repo within a group
656 joins &raquo; on each group to create a fancy link
659 joins &raquo; on each group to create a fancy link
657
660
658 ex::
661 ex::
659 group >> subgroup >> repo
662 group >> subgroup >> repo
660
663
661 :param groups_and_repos:
664 :param groups_and_repos:
662 """
665 """
663 groups, repo_name = groups_and_repos
666 groups, repo_name = groups_and_repos
664
667
665 if not groups:
668 if not groups:
666 return repo_name
669 return repo_name
667 else:
670 else:
668 def make_link(group):
671 def make_link(group):
669 return link_to(group.name, url('repos_group_home',
672 return link_to(group.name, url('repos_group_home',
670 group_name=group.group_name))
673 group_name=group.group_name))
671 return literal(' &raquo; '.join(map(make_link, groups)) + \
674 return literal(' &raquo; '.join(map(make_link, groups)) + \
672 " &raquo; " + repo_name)
675 " &raquo; " + repo_name)
673
676
674 def fancy_file_stats(stats):
677 def fancy_file_stats(stats):
675 """
678 """
676 Displays a fancy two colored bar for number of added/deleted
679 Displays a fancy two colored bar for number of added/deleted
677 lines of code on file
680 lines of code on file
678
681
679 :param stats: two element list of added/deleted lines of code
682 :param stats: two element list of added/deleted lines of code
680 """
683 """
681
684
682 a, d, t = stats[0], stats[1], stats[0] + stats[1]
685 a, d, t = stats[0], stats[1], stats[0] + stats[1]
683 width = 100
686 width = 100
684 unit = float(width) / (t or 1)
687 unit = float(width) / (t or 1)
685
688
686 # needs > 9% of width to be visible or 0 to be hidden
689 # needs > 9% of width to be visible or 0 to be hidden
687 a_p = max(9, unit * a) if a > 0 else 0
690 a_p = max(9, unit * a) if a > 0 else 0
688 d_p = max(9, unit * d) if d > 0 else 0
691 d_p = max(9, unit * d) if d > 0 else 0
689 p_sum = a_p + d_p
692 p_sum = a_p + d_p
690
693
691 if p_sum > width:
694 if p_sum > width:
692 #adjust the percentage to be == 100% since we adjusted to 9
695 #adjust the percentage to be == 100% since we adjusted to 9
693 if a_p > d_p:
696 if a_p > d_p:
694 a_p = a_p - (p_sum - width)
697 a_p = a_p - (p_sum - width)
695 else:
698 else:
696 d_p = d_p - (p_sum - width)
699 d_p = d_p - (p_sum - width)
697
700
698 a_v = a if a > 0 else ''
701 a_v = a if a > 0 else ''
699 d_v = d if d > 0 else ''
702 d_v = d if d > 0 else ''
700
703
701
704
702 def cgen(l_type):
705 def cgen(l_type):
703 mapping = {'tr':'top-right-rounded-corner',
706 mapping = {'tr':'top-right-rounded-corner',
704 'tl':'top-left-rounded-corner',
707 'tl':'top-left-rounded-corner',
705 'br':'bottom-right-rounded-corner',
708 'br':'bottom-right-rounded-corner',
706 'bl':'bottom-left-rounded-corner'}
709 'bl':'bottom-left-rounded-corner'}
707 map_getter = lambda x:mapping[x]
710 map_getter = lambda x:mapping[x]
708
711
709 if l_type == 'a' and d_v:
712 if l_type == 'a' and d_v:
710 #case when added and deleted are present
713 #case when added and deleted are present
711 return ' '.join(map(map_getter, ['tl', 'bl']))
714 return ' '.join(map(map_getter, ['tl', 'bl']))
712
715
713 if l_type == 'a' and not d_v:
716 if l_type == 'a' and not d_v:
714 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
717 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
715
718
716 if l_type == 'd' and a_v:
719 if l_type == 'd' and a_v:
717 return ' '.join(map(map_getter, ['tr', 'br']))
720 return ' '.join(map(map_getter, ['tr', 'br']))
718
721
719 if l_type == 'd' and not a_v:
722 if l_type == 'd' and not a_v:
720 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
723 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
721
724
722
725
723
726
724 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
727 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
725 a_p, a_v)
728 a_p, a_v)
726 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
729 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
727 d_p, d_v)
730 d_p, d_v)
728 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
731 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
729
732
730
733
731 def urlify_text(text):
734 def urlify_text(text_):
732 import re
735 import re
733
736
734 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
737 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
735 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
738 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
736
739
737 def url_func(match_obj):
740 def url_func(match_obj):
738 url_full = match_obj.groups()[0]
741 url_full = match_obj.groups()[0]
739 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
742 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
740
743
741 return literal(url_pat.sub(url_func, text))
744 return literal(url_pat.sub(url_func, text_))
742
745
746 def urlify_commit(text_):
747 import re
748 import traceback
749
750 try:
751 conf = config['app_conf']
752
753 URL_PAT = re.compile(r'%s' % conf.get('url_pat'))
754
755 if URL_PAT:
756 ISSUE_SERVER = conf.get('issue_server')
757 ISSUE_PREFIX = conf.get('issue_prefix')
758 def url_func(match_obj):
759 issue_id = match_obj.groups()[0]
760 tmpl = (
761 '<a class="%(cls)s" href="%(url)s">'
762 ' %(issue-prefix)s%(id-repr)s'
763 '</a>'
764 )
765 return tmpl % (
766 {
767 'cls':'issue-tracker-link',
768 'url':ISSUE_SERVER.replace('{id}',issue_id),
769 'id-repr':issue_id,
770 'issue-prefix':ISSUE_PREFIX,
771 'serv':ISSUE_SERVER,
772 }
773 )
774 return literal(URL_PAT.sub(url_func, text_))
775 except:
776 log.error(traceback.format_exc())
777 pass
778
779 return text_
743
780
744 def rst(source):
781 def rst(source):
745 return literal('<div class="rst-block">%s</div>' %
782 return literal('<div class="rst-block">%s</div>' %
746 MarkupRenderer.rst(source))
783 MarkupRenderer.rst(source))
747
784
748 def rst_w_mentions(source):
785 def rst_w_mentions(source):
749 """
786 """
750 Wrapped rst renderer with @mention highlighting
787 Wrapped rst renderer with @mention highlighting
751
788
752 :param source:
789 :param source:
753 """
790 """
754 return literal('<div class="rst-block">%s</div>' %
791 return literal('<div class="rst-block">%s</div>' %
755 MarkupRenderer.rst_with_mentions(source))
792 MarkupRenderer.rst_with_mentions(source))
@@ -1,11 +1,13 b''
1
1
2
2 class InvalidMessage(RuntimeError):
3 class InvalidMessage(RuntimeError):
3 """
4 """
4 Raised if message is missing vital headers, such
5 Raised if message is missing vital headers, such
5 as recipients or sender address.
6 as recipients or sender address.
6 """
7 """
7
8
9
8 class BadHeaders(RuntimeError):
10 class BadHeaders(RuntimeError):
9 """
11 """
10 Raised if message contains newlines in headers.
12 Raised if message contains newlines in headers.
11 """
13 """
@@ -1,180 +1,185 b''
1 from rhodecode.lib.rcmail.response import MailResponse
1 from rhodecode.lib.rcmail.response import MailResponse
2
2
3 from rhodecode.lib.rcmail.exceptions import BadHeaders
3 from rhodecode.lib.rcmail.exceptions import BadHeaders
4 from rhodecode.lib.rcmail.exceptions import InvalidMessage
4 from rhodecode.lib.rcmail.exceptions import InvalidMessage
5
5
6 class Attachment(object):
6 class Attachment(object):
7 """
7 """
8 Encapsulates file attachment information.
8 Encapsulates file attachment information.
9
9
10 :param filename: filename of attachment
10 :param filename: filename of attachment
11 :param content_type: file mimetype
11 :param content_type: file mimetype
12 :param data: the raw file data, either as string or file obj
12 :param data: the raw file data, either as string or file obj
13 :param disposition: content-disposition (if any)
13 :param disposition: content-disposition (if any)
14 """
14 """
15
15
16 def __init__(self,
16 def __init__(self,
17 filename=None,
17 filename=None,
18 content_type=None,
18 content_type=None,
19 data=None,
19 data=None,
20 disposition=None):
20 disposition=None):
21
21
22 self.filename = filename
22 self.filename = filename
23 self.content_type = content_type
23 self.content_type = content_type
24 self.disposition = disposition or 'attachment'
24 self.disposition = disposition or 'attachment'
25 self._data = data
25 self._data = data
26
26
27 @property
27 @property
28 def data(self):
28 def data(self):
29 if isinstance(self._data, basestring):
29 if isinstance(self._data, basestring):
30 return self._data
30 return self._data
31 self._data = self._data.read()
31 self._data = self._data.read()
32 return self._data
32 return self._data
33
33
34
34
35 class Message(object):
35 class Message(object):
36 """
36 """
37 Encapsulates an email message.
37 Encapsulates an email message.
38
38
39 :param subject: email subject header
39 :param subject: email subject header
40 :param recipients: list of email addresses
40 :param recipients: list of email addresses
41 :param body: plain text message
41 :param body: plain text message
42 :param html: HTML message
42 :param html: HTML message
43 :param sender: email sender address
43 :param sender: email sender address
44 :param cc: CC list
44 :param cc: CC list
45 :param bcc: BCC list
45 :param bcc: BCC list
46 :param extra_headers: dict of extra email headers
46 :param extra_headers: dict of extra email headers
47 :param attachments: list of Attachment instances
47 :param attachments: list of Attachment instances
48 :param recipients_separator: alternative separator for any of
49 'From', 'To', 'Delivered-To', 'Cc', 'Bcc' fields
48 """
50 """
49
51
50 def __init__(self,
52 def __init__(self,
51 subject=None,
53 subject=None,
52 recipients=None,
54 recipients=None,
53 body=None,
55 body=None,
54 html=None,
56 html=None,
55 sender=None,
57 sender=None,
56 cc=None,
58 cc=None,
57 bcc=None,
59 bcc=None,
58 extra_headers=None,
60 extra_headers=None,
59 attachments=None):
61 attachments=None,
60
62 recipients_separator="; "):
61
63
62 self.subject = subject or ''
64 self.subject = subject or ''
63 self.sender = sender
65 self.sender = sender
64 self.body = body
66 self.body = body
65 self.html = html
67 self.html = html
66
68
67 self.recipients = recipients or []
69 self.recipients = recipients or []
68 self.attachments = attachments or []
70 self.attachments = attachments or []
69 self.cc = cc or []
71 self.cc = cc or []
70 self.bcc = bcc or []
72 self.bcc = bcc or []
71 self.extra_headers = extra_headers or {}
73 self.extra_headers = extra_headers or {}
72
74
75 self.recipients_separator = recipients_separator
76
73 @property
77 @property
74 def send_to(self):
78 def send_to(self):
75 return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
79 return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
76
80
77 def to_message(self):
81 def to_message(self):
78 """
82 """
79 Returns raw email.Message instance.Validates message first.
83 Returns raw email.Message instance.Validates message first.
80 """
84 """
81
85
82 self.validate()
86 self.validate()
83
87
84 return self.get_response().to_message()
88 return self.get_response().to_message()
85
89
86 def get_response(self):
90 def get_response(self):
87 """
91 """
88 Creates a Lamson MailResponse instance
92 Creates a Lamson MailResponse instance
89 """
93 """
90
94
91 response = MailResponse(Subject=self.subject,
95 response = MailResponse(Subject=self.subject,
92 To=self.recipients,
96 To=self.recipients,
93 From=self.sender,
97 From=self.sender,
94 Body=self.body,
98 Body=self.body,
95 Html=self.html)
99 Html=self.html,
100 separator=self.recipients_separator)
96
101
97 if self.bcc:
102 if self.bcc:
98 response.base['Bcc'] = self.bcc
103 response.base['Bcc'] = self.bcc
99
104
100 if self.cc:
105 if self.cc:
101 response.base['Cc'] = self.cc
106 response.base['Cc'] = self.cc
102
107
103 for attachment in self.attachments:
108 for attachment in self.attachments:
104
109
105 response.attach(attachment.filename,
110 response.attach(attachment.filename,
106 attachment.content_type,
111 attachment.content_type,
107 attachment.data,
112 attachment.data,
108 attachment.disposition)
113 attachment.disposition)
109
114
110 response.update(self.extra_headers)
115 response.update(self.extra_headers)
111
116
112 return response
117 return response
113
118
114 def is_bad_headers(self):
119 def is_bad_headers(self):
115 """
120 """
116 Checks for bad headers i.e. newlines in subject, sender or recipients.
121 Checks for bad headers i.e. newlines in subject, sender or recipients.
117 """
122 """
118
123
119 headers = [self.subject, self.sender]
124 headers = [self.subject, self.sender]
120 headers += list(self.send_to)
125 headers += list(self.send_to)
121 headers += self.extra_headers.values()
126 headers += self.extra_headers.values()
122
127
123 for val in headers:
128 for val in headers:
124 for c in '\r\n':
129 for c in '\r\n':
125 if c in val:
130 if c in val:
126 return True
131 return True
127 return False
132 return False
128
133
129 def validate(self):
134 def validate(self):
130 """
135 """
131 Checks if message is valid and raises appropriate exception.
136 Checks if message is valid and raises appropriate exception.
132 """
137 """
133
138
134 if not self.recipients:
139 if not self.recipients:
135 raise InvalidMessage, "No recipients have been added"
140 raise InvalidMessage, "No recipients have been added"
136
141
137 if not self.body and not self.html:
142 if not self.body and not self.html:
138 raise InvalidMessage, "No body has been set"
143 raise InvalidMessage, "No body has been set"
139
144
140 if not self.sender:
145 if not self.sender:
141 raise InvalidMessage, "No sender address has been set"
146 raise InvalidMessage, "No sender address has been set"
142
147
143 if self.is_bad_headers():
148 if self.is_bad_headers():
144 raise BadHeaders
149 raise BadHeaders
145
150
146 def add_recipient(self, recipient):
151 def add_recipient(self, recipient):
147 """
152 """
148 Adds another recipient to the message.
153 Adds another recipient to the message.
149
154
150 :param recipient: email address of recipient.
155 :param recipient: email address of recipient.
151 """
156 """
152
157
153 self.recipients.append(recipient)
158 self.recipients.append(recipient)
154
159
155 def add_cc(self, recipient):
160 def add_cc(self, recipient):
156 """
161 """
157 Adds an email address to the CC list.
162 Adds an email address to the CC list.
158
163
159 :param recipient: email address of recipient.
164 :param recipient: email address of recipient.
160 """
165 """
161
166
162 self.cc.append(recipient)
167 self.cc.append(recipient)
163
168
164 def add_bcc(self, recipient):
169 def add_bcc(self, recipient):
165 """
170 """
166 Adds an email address to the BCC list.
171 Adds an email address to the BCC list.
167
172
168 :param recipient: email address of recipient.
173 :param recipient: email address of recipient.
169 """
174 """
170
175
171 self.bcc.append(recipient)
176 self.bcc.append(recipient)
172
177
173 def attach(self, attachment):
178 def attach(self, attachment):
174 """
179 """
175 Adds an attachment to the message.
180 Adds an attachment to the message.
176
181
177 :param attachment: an **Attachment** instance.
182 :param attachment: an **Attachment** instance.
178 """
183 """
179
184
180 self.attachments.append(attachment)
185 self.attachments.append(attachment)
@@ -1,438 +1,446 b''
1 # The code in this module is entirely lifted from the Lamson project
1 # The code in this module is entirely lifted from the Lamson project
2 # (http://lamsonproject.org/). Its copyright is:
2 # (http://lamsonproject.org/). Its copyright is:
3
3
4 # Copyright (c) 2008, Zed A. Shaw
4 # Copyright (c) 2008, Zed A. Shaw
5 # All rights reserved.
5 # All rights reserved.
6
6
7 # It is provided under this license:
7 # It is provided under this license:
8
8
9 # Redistribution and use in source and binary forms, with or without
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted provided that the following conditions are met:
10 # modification, are permitted provided that the following conditions are met:
11
11
12 # * Redistributions of source code must retain the above copyright notice, this
12 # * Redistributions of source code must retain the above copyright notice, this
13 # list of conditions and the following disclaimer.
13 # list of conditions and the following disclaimer.
14
14
15 # * Redistributions in binary form must reproduce the above copyright notice,
15 # * Redistributions in binary form must reproduce the above copyright notice,
16 # this list of conditions and the following disclaimer in the documentation
16 # this list of conditions and the following disclaimer in the documentation
17 # and/or other materials provided with the distribution.
17 # and/or other materials provided with the distribution.
18
18
19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
20 # be used to endorse or promote products derived from this software without
20 # be used to endorse or promote products derived from this software without
21 # specific prior written permission.
21 # specific prior written permission.
22
22
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 # POSSIBILITY OF SUCH DAMAGE.
34 # POSSIBILITY OF SUCH DAMAGE.
35
35
36 import os
36 import os
37 import mimetypes
37 import mimetypes
38 import string
38 import string
39 from email import encoders
39 from email import encoders
40 from email.charset import Charset
40 from email.charset import Charset
41 from email.utils import parseaddr
41 from email.utils import parseaddr
42 from email.mime.base import MIMEBase
42 from email.mime.base import MIMEBase
43
43
44 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc', 'Bcc']
44 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc', 'Bcc']
45 DEFAULT_ENCODING = "utf-8"
45 DEFAULT_ENCODING = "utf-8"
46 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
46 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
47
47
48 def normalize_header(header):
48 def normalize_header(header):
49 return string.capwords(header.lower(), '-')
49 return string.capwords(header.lower(), '-')
50
50
51 class EncodingError(Exception):
51 class EncodingError(Exception):
52 """Thrown when there is an encoding error."""
52 """Thrown when there is an encoding error."""
53 pass
53 pass
54
54
55 class MailBase(object):
55 class MailBase(object):
56 """MailBase is used as the basis of lamson.mail and contains the basics of
56 """MailBase is used as the basis of lamson.mail and contains the basics of
57 encoding an email. You actually can do all your email processing with this
57 encoding an email. You actually can do all your email processing with this
58 class, but it's more raw.
58 class, but it's more raw.
59 """
59 """
60 def __init__(self, items=()):
60 def __init__(self, items=()):
61 self.headers = dict(items)
61 self.headers = dict(items)
62 self.parts = []
62 self.parts = []
63 self.body = None
63 self.body = None
64 self.content_encoding = {'Content-Type': (None, {}),
64 self.content_encoding = {'Content-Type': (None, {}),
65 'Content-Disposition': (None, {}),
65 'Content-Disposition': (None, {}),
66 'Content-Transfer-Encoding': (None, {})}
66 'Content-Transfer-Encoding': (None, {})}
67
67
68 def __getitem__(self, key):
68 def __getitem__(self, key):
69 return self.headers.get(normalize_header(key), None)
69 return self.headers.get(normalize_header(key), None)
70
70
71 def __len__(self):
71 def __len__(self):
72 return len(self.headers)
72 return len(self.headers)
73
73
74 def __iter__(self):
74 def __iter__(self):
75 return iter(self.headers)
75 return iter(self.headers)
76
76
77 def __contains__(self, key):
77 def __contains__(self, key):
78 return normalize_header(key) in self.headers
78 return normalize_header(key) in self.headers
79
79
80 def __setitem__(self, key, value):
80 def __setitem__(self, key, value):
81 self.headers[normalize_header(key)] = value
81 self.headers[normalize_header(key)] = value
82
82
83 def __delitem__(self, key):
83 def __delitem__(self, key):
84 del self.headers[normalize_header(key)]
84 del self.headers[normalize_header(key)]
85
85
86 def __nonzero__(self):
86 def __nonzero__(self):
87 return self.body != None or len(self.headers) > 0 or len(self.parts) > 0
87 return self.body != None or len(self.headers) > 0 or len(self.parts) > 0
88
88
89 def keys(self):
89 def keys(self):
90 """Returns the sorted keys."""
90 """Returns the sorted keys."""
91 return sorted(self.headers.keys())
91 return sorted(self.headers.keys())
92
92
93 def attach_file(self, filename, data, ctype, disposition):
93 def attach_file(self, filename, data, ctype, disposition):
94 """
94 """
95 A file attachment is a raw attachment with a disposition that
95 A file attachment is a raw attachment with a disposition that
96 indicates the file name.
96 indicates the file name.
97 """
97 """
98 assert filename, "You can't attach a file without a filename."
98 assert filename, "You can't attach a file without a filename."
99 ctype = ctype.lower()
99 ctype = ctype.lower()
100
100
101 part = MailBase()
101 part = MailBase()
102 part.body = data
102 part.body = data
103 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
103 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
104 part.content_encoding['Content-Disposition'] = (disposition,
104 part.content_encoding['Content-Disposition'] = (disposition,
105 {'filename': filename})
105 {'filename': filename})
106 self.parts.append(part)
106 self.parts.append(part)
107
107
108
108
109 def attach_text(self, data, ctype):
109 def attach_text(self, data, ctype):
110 """
110 """
111 This attaches a simpler text encoded part, which doesn't have a
111 This attaches a simpler text encoded part, which doesn't have a
112 filename.
112 filename.
113 """
113 """
114 ctype = ctype.lower()
114 ctype = ctype.lower()
115
115
116 part = MailBase()
116 part = MailBase()
117 part.body = data
117 part.body = data
118 part.content_encoding['Content-Type'] = (ctype, {})
118 part.content_encoding['Content-Type'] = (ctype, {})
119 self.parts.append(part)
119 self.parts.append(part)
120
120
121 def walk(self):
121 def walk(self):
122 for p in self.parts:
122 for p in self.parts:
123 yield p
123 yield p
124 for x in p.walk():
124 for x in p.walk():
125 yield x
125 yield x
126
126
127 class MailResponse(object):
127 class MailResponse(object):
128 """
128 """
129 You are given MailResponse objects from the lamson.view methods, and
129 You are given MailResponse objects from the lamson.view methods, and
130 whenever you want to generate an email to send to someone. It has the
130 whenever you want to generate an email to send to someone. It has the
131 same basic functionality as MailRequest, but it is designed to be written
131 same basic functionality as MailRequest, but it is designed to be written
132 to, rather than read from (although you can do both).
132 to, rather than read from (although you can do both).
133
133
134 You can easily set a Body or Html during creation or after by passing it
134 You can easily set a Body or Html during creation or after by passing it
135 as __init__ parameters, or by setting those attributes.
135 as __init__ parameters, or by setting those attributes.
136
136
137 You can initially set the From, To, and Subject, but they are headers so
137 You can initially set the From, To, and Subject, but they are headers so
138 use the dict notation to change them: msg['From'] = 'joe@test.com'.
138 use the dict notation to change them: msg['From'] = 'joe@test.com'.
139
139
140 The message is not fully crafted until right when you convert it with
140 The message is not fully crafted until right when you convert it with
141 MailResponse.to_message. This lets you change it and work with it, then
141 MailResponse.to_message. This lets you change it and work with it, then
142 send it out when it's ready.
142 send it out when it's ready.
143 """
143 """
144 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None):
144 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None,
145 separator="; "):
145 self.Body = Body
146 self.Body = Body
146 self.Html = Html
147 self.Html = Html
147 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
148 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
148 self.multipart = self.Body and self.Html
149 self.multipart = self.Body and self.Html
149 self.attachments = []
150 self.attachments = []
151 self.separator = separator
150
152
151 def __contains__(self, key):
153 def __contains__(self, key):
152 return self.base.__contains__(key)
154 return self.base.__contains__(key)
153
155
154 def __getitem__(self, key):
156 def __getitem__(self, key):
155 return self.base.__getitem__(key)
157 return self.base.__getitem__(key)
156
158
157 def __setitem__(self, key, val):
159 def __setitem__(self, key, val):
158 return self.base.__setitem__(key, val)
160 return self.base.__setitem__(key, val)
159
161
160 def __delitem__(self, name):
162 def __delitem__(self, name):
161 del self.base[name]
163 del self.base[name]
162
164
163 def attach(self, filename=None, content_type=None, data=None,
165 def attach(self, filename=None, content_type=None, data=None,
164 disposition=None):
166 disposition=None):
165 """
167 """
166
168
167 Simplifies attaching files from disk or data as files. To attach
169 Simplifies attaching files from disk or data as files. To attach
168 simple text simple give data and a content_type. To attach a file,
170 simple text simple give data and a content_type. To attach a file,
169 give the data/content_type/filename/disposition combination.
171 give the data/content_type/filename/disposition combination.
170
172
171 For convenience, if you don't give data and only a filename, then it
173 For convenience, if you don't give data and only a filename, then it
172 will read that file's contents when you call to_message() later. If
174 will read that file's contents when you call to_message() later. If
173 you give data and filename then it will assume you've filled data
175 you give data and filename then it will assume you've filled data
174 with what the file's contents are and filename is just the name to
176 with what the file's contents are and filename is just the name to
175 use.
177 use.
176 """
178 """
177
179
178 assert filename or data, ("You must give a filename or some data to "
180 assert filename or data, ("You must give a filename or some data to "
179 "attach.")
181 "attach.")
180 assert data or os.path.exists(filename), ("File doesn't exist, and no "
182 assert data or os.path.exists(filename), ("File doesn't exist, and no "
181 "data given.")
183 "data given.")
182
184
183 self.multipart = True
185 self.multipart = True
184
186
185 if filename and not content_type:
187 if filename and not content_type:
186 content_type, encoding = mimetypes.guess_type(filename)
188 content_type, encoding = mimetypes.guess_type(filename)
187
189
188 assert content_type, ("No content type given, and couldn't guess "
190 assert content_type, ("No content type given, and couldn't guess "
189 "from the filename: %r" % filename)
191 "from the filename: %r" % filename)
190
192
191 self.attachments.append({'filename': filename,
193 self.attachments.append({'filename': filename,
192 'content_type': content_type,
194 'content_type': content_type,
193 'data': data,
195 'data': data,
194 'disposition': disposition,})
196 'disposition': disposition,})
195 def attach_part(self, part):
197 def attach_part(self, part):
196 """
198 """
197 Attaches a raw MailBase part from a MailRequest (or anywhere)
199 Attaches a raw MailBase part from a MailRequest (or anywhere)
198 so that you can copy it over.
200 so that you can copy it over.
199 """
201 """
200 self.multipart = True
202 self.multipart = True
201
203
202 self.attachments.append({'filename': None,
204 self.attachments.append({'filename': None,
203 'content_type': None,
205 'content_type': None,
204 'data': None,
206 'data': None,
205 'disposition': None,
207 'disposition': None,
206 'part': part,
208 'part': part,
207 })
209 })
208
210
209 def attach_all_parts(self, mail_request):
211 def attach_all_parts(self, mail_request):
210 """
212 """
211 Used for copying the attachment parts of a mail.MailRequest
213 Used for copying the attachment parts of a mail.MailRequest
212 object for mailing lists that need to maintain attachments.
214 object for mailing lists that need to maintain attachments.
213 """
215 """
214 for part in mail_request.all_parts():
216 for part in mail_request.all_parts():
215 self.attach_part(part)
217 self.attach_part(part)
216
218
217 self.base.content_encoding = mail_request.base.content_encoding.copy()
219 self.base.content_encoding = mail_request.base.content_encoding.copy()
218
220
219 def clear(self):
221 def clear(self):
220 """
222 """
221 Clears out the attachments so you can redo them. Use this to keep the
223 Clears out the attachments so you can redo them. Use this to keep the
222 headers for a series of different messages with different attachments.
224 headers for a series of different messages with different attachments.
223 """
225 """
224 del self.attachments[:]
226 del self.attachments[:]
225 del self.base.parts[:]
227 del self.base.parts[:]
226 self.multipart = False
228 self.multipart = False
227
229
228
230
229 def update(self, message):
231 def update(self, message):
230 """
232 """
231 Used to easily set a bunch of heading from another dict
233 Used to easily set a bunch of heading from another dict
232 like object.
234 like object.
233 """
235 """
234 for k in message.keys():
236 for k in message.keys():
235 self.base[k] = message[k]
237 self.base[k] = message[k]
236
238
237 def __str__(self):
239 def __str__(self):
238 """
240 """
239 Converts to a string.
241 Converts to a string.
240 """
242 """
241 return self.to_message().as_string()
243 return self.to_message().as_string()
242
244
243 def _encode_attachment(self, filename=None, content_type=None, data=None,
245 def _encode_attachment(self, filename=None, content_type=None, data=None,
244 disposition=None, part=None):
246 disposition=None, part=None):
245 """
247 """
246 Used internally to take the attachments mentioned in self.attachments
248 Used internally to take the attachments mentioned in self.attachments
247 and do the actual encoding in a lazy way when you call to_message.
249 and do the actual encoding in a lazy way when you call to_message.
248 """
250 """
249 if part:
251 if part:
250 self.base.parts.append(part)
252 self.base.parts.append(part)
251 elif filename:
253 elif filename:
252 if not data:
254 if not data:
253 data = open(filename).read()
255 data = open(filename).read()
254
256
255 self.base.attach_file(filename, data, content_type,
257 self.base.attach_file(filename, data, content_type,
256 disposition or 'attachment')
258 disposition or 'attachment')
257 else:
259 else:
258 self.base.attach_text(data, content_type)
260 self.base.attach_text(data, content_type)
259
261
260 ctype = self.base.content_encoding['Content-Type'][0]
262 ctype = self.base.content_encoding['Content-Type'][0]
261
263
262 if ctype and not ctype.startswith('multipart'):
264 if ctype and not ctype.startswith('multipart'):
263 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
265 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
264
266
265 def to_message(self):
267 def to_message(self):
266 """
268 """
267 Figures out all the required steps to finally craft the
269 Figures out all the required steps to finally craft the
268 message you need and return it. The resulting message
270 message you need and return it. The resulting message
269 is also available as a self.base attribute.
271 is also available as a self.base attribute.
270
272
271 What is returned is a Python email API message you can
273 What is returned is a Python email API message you can
272 use with those APIs. The self.base attribute is the raw
274 use with those APIs. The self.base attribute is the raw
273 lamson.encoding.MailBase.
275 lamson.encoding.MailBase.
274 """
276 """
275 del self.base.parts[:]
277 del self.base.parts[:]
276
278
277 if self.Body and self.Html:
279 if self.Body and self.Html:
278 self.multipart = True
280 self.multipart = True
279 self.base.content_encoding['Content-Type'] = (
281 self.base.content_encoding['Content-Type'] = (
280 'multipart/alternative', {})
282 'multipart/alternative', {})
281
283
282 if self.multipart:
284 if self.multipart:
283 self.base.body = None
285 self.base.body = None
284 if self.Body:
286 if self.Body:
285 self.base.attach_text(self.Body, 'text/plain')
287 self.base.attach_text(self.Body, 'text/plain')
286
288
287 if self.Html:
289 if self.Html:
288 self.base.attach_text(self.Html, 'text/html')
290 self.base.attach_text(self.Html, 'text/html')
289
291
290 for args in self.attachments:
292 for args in self.attachments:
291 self._encode_attachment(**args)
293 self._encode_attachment(**args)
292
294
293 elif self.Body:
295 elif self.Body:
294 self.base.body = self.Body
296 self.base.body = self.Body
295 self.base.content_encoding['Content-Type'] = ('text/plain', {})
297 self.base.content_encoding['Content-Type'] = ('text/plain', {})
296
298
297 elif self.Html:
299 elif self.Html:
298 self.base.body = self.Html
300 self.base.body = self.Html
299 self.base.content_encoding['Content-Type'] = ('text/html', {})
301 self.base.content_encoding['Content-Type'] = ('text/html', {})
300
302
301 return to_message(self.base)
303 return to_message(self.base, separator=self.separator)
302
304
303 def all_parts(self):
305 def all_parts(self):
304 """
306 """
305 Returns all the encoded parts. Only useful for debugging
307 Returns all the encoded parts. Only useful for debugging
306 or inspecting after calling to_message().
308 or inspecting after calling to_message().
307 """
309 """
308 return self.base.parts
310 return self.base.parts
309
311
310 def keys(self):
312 def keys(self):
311 return self.base.keys()
313 return self.base.keys()
312
314
313 def to_message(mail):
315 def to_message(mail, separator="; "):
314 """
316 """
315 Given a MailBase message, this will construct a MIMEPart
317 Given a MailBase message, this will construct a MIMEPart
316 that is canonicalized for use with the Python email API.
318 that is canonicalized for use with the Python email API.
317 """
319 """
318 ctype, params = mail.content_encoding['Content-Type']
320 ctype, params = mail.content_encoding['Content-Type']
319
321
320 if not ctype:
322 if not ctype:
321 if mail.parts:
323 if mail.parts:
322 ctype = 'multipart/mixed'
324 ctype = 'multipart/mixed'
323 else:
325 else:
324 ctype = 'text/plain'
326 ctype = 'text/plain'
325 else:
327 else:
326 if mail.parts:
328 if mail.parts:
327 assert ctype.startswith(("multipart", "message")), \
329 assert ctype.startswith(("multipart", "message")), \
328 "Content type should be multipart or message, not %r" % ctype
330 "Content type should be multipart or message, not %r" % ctype
329
331
330 # adjust the content type according to what it should be now
332 # adjust the content type according to what it should be now
331 mail.content_encoding['Content-Type'] = (ctype, params)
333 mail.content_encoding['Content-Type'] = (ctype, params)
332
334
333 try:
335 try:
334 out = MIMEPart(ctype, **params)
336 out = MIMEPart(ctype, **params)
335 except TypeError, exc: # pragma: no cover
337 except TypeError, exc: # pragma: no cover
336 raise EncodingError("Content-Type malformed, not allowed: %r; "
338 raise EncodingError("Content-Type malformed, not allowed: %r; "
337 "%r (Python ERROR: %s" %
339 "%r (Python ERROR: %s" %
338 (ctype, params, exc.message))
340 (ctype, params, exc.message))
339
341
340 for k in mail.keys():
342 for k in mail.keys():
341 if k in ADDRESS_HEADERS_WHITELIST:
343 if k in ADDRESS_HEADERS_WHITELIST:
342 out[k.encode('ascii')] = header_to_mime_encoding(mail[k])
344 out[k.encode('ascii')] = header_to_mime_encoding(
345 mail[k],
346 not_email=False,
347 separator=separator
348 )
343 else:
349 else:
344 out[k.encode('ascii')] = header_to_mime_encoding(mail[k],
350 out[k.encode('ascii')] = header_to_mime_encoding(
345 not_email=True)
351 mail[k],
352 not_email=True
353 )
346
354
347 out.extract_payload(mail)
355 out.extract_payload(mail)
348
356
349 # go through the children
357 # go through the children
350 for part in mail.parts:
358 for part in mail.parts:
351 out.attach(to_message(part))
359 out.attach(to_message(part))
352
360
353 return out
361 return out
354
362
355 class MIMEPart(MIMEBase):
363 class MIMEPart(MIMEBase):
356 """
364 """
357 A reimplementation of nearly everything in email.mime to be more useful
365 A reimplementation of nearly everything in email.mime to be more useful
358 for actually attaching things. Rather than one class for every type of
366 for actually attaching things. Rather than one class for every type of
359 thing you'd encode, there's just this one, and it figures out how to
367 thing you'd encode, there's just this one, and it figures out how to
360 encode what you ask it.
368 encode what you ask it.
361 """
369 """
362 def __init__(self, type, **params):
370 def __init__(self, type, **params):
363 self.maintype, self.subtype = type.split('/')
371 self.maintype, self.subtype = type.split('/')
364 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
372 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
365
373
366 def add_text(self, content):
374 def add_text(self, content):
367 # this is text, so encode it in canonical form
375 # this is text, so encode it in canonical form
368 try:
376 try:
369 encoded = content.encode('ascii')
377 encoded = content.encode('ascii')
370 charset = 'ascii'
378 charset = 'ascii'
371 except UnicodeError:
379 except UnicodeError:
372 encoded = content.encode('utf-8')
380 encoded = content.encode('utf-8')
373 charset = 'utf-8'
381 charset = 'utf-8'
374
382
375 self.set_payload(encoded, charset=charset)
383 self.set_payload(encoded, charset=charset)
376
384
377
385
378 def extract_payload(self, mail):
386 def extract_payload(self, mail):
379 if mail.body == None: return # only None, '' is still ok
387 if mail.body == None: return # only None, '' is still ok
380
388
381 ctype, ctype_params = mail.content_encoding['Content-Type']
389 ctype, ctype_params = mail.content_encoding['Content-Type']
382 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
390 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
383
391
384 assert ctype, ("Extract payload requires that mail.content_encoding "
392 assert ctype, ("Extract payload requires that mail.content_encoding "
385 "have a valid Content-Type.")
393 "have a valid Content-Type.")
386
394
387 if ctype.startswith("text/"):
395 if ctype.startswith("text/"):
388 self.add_text(mail.body)
396 self.add_text(mail.body)
389 else:
397 else:
390 if cdisp:
398 if cdisp:
391 # replicate the content-disposition settings
399 # replicate the content-disposition settings
392 self.add_header('Content-Disposition', cdisp, **cdisp_params)
400 self.add_header('Content-Disposition', cdisp, **cdisp_params)
393
401
394 self.set_payload(mail.body)
402 self.set_payload(mail.body)
395 encoders.encode_base64(self)
403 encoders.encode_base64(self)
396
404
397 def __repr__(self):
405 def __repr__(self):
398 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
406 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
399 self.subtype,
407 self.subtype,
400 self.maintype,
408 self.maintype,
401 self['Content-Type'],
409 self['Content-Type'],
402 self['Content-Disposition'],
410 self['Content-Disposition'],
403 self.is_multipart())
411 self.is_multipart())
404
412
405
413
406 def header_to_mime_encoding(value, not_email=False):
414 def header_to_mime_encoding(value, not_email=False, separator=", "):
407 if not value: return ""
415 if not value: return ""
408
416
409 encoder = Charset(DEFAULT_ENCODING)
417 encoder = Charset(DEFAULT_ENCODING)
410 if type(value) == list:
418 if type(value) == list:
411 return "; ".join(properly_encode_header(
419 return separator.join(properly_encode_header(
412 v, encoder, not_email) for v in value)
420 v, encoder, not_email) for v in value)
413 else:
421 else:
414 return properly_encode_header(value, encoder, not_email)
422 return properly_encode_header(value, encoder, not_email)
415
423
416 def properly_encode_header(value, encoder, not_email):
424 def properly_encode_header(value, encoder, not_email):
417 """
425 """
418 The only thing special (weird) about this function is that it tries
426 The only thing special (weird) about this function is that it tries
419 to do a fast check to see if the header value has an email address in
427 to do a fast check to see if the header value has an email address in
420 it. Since random headers could have an email address, and email addresses
428 it. Since random headers could have an email address, and email addresses
421 have weird special formatting rules, we have to check for it.
429 have weird special formatting rules, we have to check for it.
422
430
423 Normally this works fine, but in Librelist, we need to "obfuscate" email
431 Normally this works fine, but in Librelist, we need to "obfuscate" email
424 addresses by changing the '@' to '-AT-'. This is where
432 addresses by changing the '@' to '-AT-'. This is where
425 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False
433 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False
426 to check if a header value has an email address. If you need to make this
434 to check if a header value has an email address. If you need to make this
427 check different, then change this.
435 check different, then change this.
428 """
436 """
429 try:
437 try:
430 return value.encode("ascii")
438 return value.encode("ascii")
431 except UnicodeEncodeError:
439 except UnicodeEncodeError:
432 if not_email is False and VALUE_IS_EMAIL_ADDRESS(value):
440 if not_email is False and VALUE_IS_EMAIL_ADDRESS(value):
433 # this could have an email address, make sure we don't screw it up
441 # this could have an email address, make sure we don't screw it up
434 name, address = parseaddr(value)
442 name, address = parseaddr(value)
435 return '"%s" <%s>' % (
443 return '"%s" <%s>' % (
436 encoder.header_encode(name.encode("utf-8")), address)
444 encoder.header_encode(name.encode("utf-8")), address)
437
445
438 return encoder.header_encode(value.encode("utf-8"))
446 return encoder.header_encode(value.encode("utf-8"))
@@ -1,93 +1,94 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.rcmail.smtp_mailer
3 rhodecode.lib.rcmail.smtp_mailer
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Simple smtp mailer used in RhodeCode
6 Simple smtp mailer used in RhodeCode
7
7
8 :created_on: Sep 13, 2010
8 :created_on: Sep 13, 2010
9 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
9 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :license: GPLv3, see COPYING for more details.
10 :license: GPLv3, see COPYING for more details.
11 """
11 """
12 # This program is free software: you can redistribute it and/or modify
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
15 # (at your option) any later version.
16 #
16 #
17 # This program is distributed in the hope that it will be useful,
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
20 # GNU General Public License for more details.
21 #
21 #
22 # You should have received a copy of the GNU General Public License
22 # You should have received a copy of the GNU General Public License
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24
24
25 import logging
25 import logging
26 import smtplib
26 import smtplib
27 from socket import sslerror
27 from socket import sslerror
28 from rhodecode.lib.rcmail.message import Message
28 from rhodecode.lib.rcmail.message import Message
29
29
30
30
31 class SmtpMailer(object):
31 class SmtpMailer(object):
32 """SMTP mailer class
32 """SMTP mailer class
33
33
34 mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
34 mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
35 mail_port, ssl, tls)
35 mail_port, ssl, tls)
36 mailer.send(recipients, subject, body, attachment_files)
36 mailer.send(recipients, subject, body, attachment_files)
37
37
38 :param recipients might be a list of string or single string
38 :param recipients might be a list of string or single string
39 :param attachment_files is a dict of {filename:location}
39 :param attachment_files is a dict of {filename:location}
40 it tries to guess the mimetype and attach the file
40 it tries to guess the mimetype and attach the file
41
41
42 """
42 """
43
43
44 def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
44 def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
45 mail_port=None, ssl=False, tls=False, debug=False):
45 mail_port=None, ssl=False, tls=False, debug=False):
46
46
47 self.mail_from = mail_from
47 self.mail_from = mail_from
48 self.mail_server = mail_server
48 self.mail_server = mail_server
49 self.mail_port = mail_port
49 self.mail_port = mail_port
50 self.user = user
50 self.user = user
51 self.passwd = passwd
51 self.passwd = passwd
52 self.ssl = ssl
52 self.ssl = ssl
53 self.tls = tls
53 self.tls = tls
54 self.debug = debug
54 self.debug = debug
55 self.auth = smtp_auth
55 self.auth = smtp_auth
56
56
57 def send(self, recipients=[], subject='', body='', html='',
57 def send(self, recipients=[], subject='', body='', html='',
58 attachment_files=None):
58 attachment_files=None):
59
59
60 if isinstance(recipients, basestring):
60 if isinstance(recipients, basestring):
61 recipients = [recipients]
61 recipients = [recipients]
62 msg = Message(subject, recipients, body, html, self.mail_from)
62 msg = Message(subject, recipients, body, html, self.mail_from,
63 recipients_separator=", ")
63 raw_msg = msg.to_message()
64 raw_msg = msg.to_message()
64
65
65 if self.ssl:
66 if self.ssl:
66 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port)
67 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port)
67 else:
68 else:
68 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port)
69 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port)
69
70
70 if self.tls:
71 if self.tls:
71 smtp_serv.ehlo()
72 smtp_serv.ehlo()
72 smtp_serv.starttls()
73 smtp_serv.starttls()
73
74
74 if self.debug:
75 if self.debug:
75 smtp_serv.set_debuglevel(1)
76 smtp_serv.set_debuglevel(1)
76
77
77 smtp_serv.ehlo()
78 smtp_serv.ehlo()
78 if self.auth:
79 if self.auth:
79 smtp_serv.esmtp_features["auth"] = self.auth
80 smtp_serv.esmtp_features["auth"] = self.auth
80
81
81 # if server requires authorization you must provide login and password
82 # if server requires authorization you must provide login and password
82 # but only if we have them
83 # but only if we have them
83 if self.user and self.passwd:
84 if self.user and self.passwd:
84 smtp_serv.login(self.user, self.passwd)
85 smtp_serv.login(self.user, self.passwd)
85
86
86 smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string())
87 smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string())
87 logging.info('MAIL SEND TO: %s' % recipients)
88 logging.info('MAIL SEND TO: %s' % recipients)
88
89
89 try:
90 try:
90 smtp_serv.quit()
91 smtp_serv.quit()
91 except sslerror:
92 except sslerror:
92 # sslerror is raised in tls connections on closing sometimes
93 # sslerror is raised in tls connections on closing sometimes
93 pass
94 pass
@@ -1,599 +1,599 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.utils
3 rhodecode.lib.utils
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Utilities library for RhodeCode
6 Utilities library for RhodeCode
7
7
8 :created_on: Apr 18, 2010
8 :created_on: Apr 18, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import traceback
29 import traceback
30 import paste
30 import paste
31 import beaker
31 import beaker
32 import tarfile
32 import tarfile
33 import shutil
33 import shutil
34 from os.path import abspath
34 from os.path import abspath
35 from os.path import dirname as dn, join as jn
35 from os.path import dirname as dn, join as jn
36
36
37 from paste.script.command import Command, BadCommand
37 from paste.script.command import Command, BadCommand
38
38
39 from mercurial import ui, config
39 from mercurial import ui, config
40
40
41 from webhelpers.text import collapse, remove_formatting, strip_tags
41 from webhelpers.text import collapse, remove_formatting, strip_tags
42
42
43 from vcs import get_backend
43 from vcs import get_backend
44 from vcs.backends.base import BaseChangeset
44 from vcs.backends.base import BaseChangeset
45 from vcs.utils.lazy import LazyProperty
45 from vcs.utils.lazy import LazyProperty
46 from vcs.utils.helpers import get_scm
46 from vcs.utils.helpers import get_scm
47 from vcs.exceptions import VCSError
47 from vcs.exceptions import VCSError
48
48
49 from rhodecode.lib.caching_query import FromCache
49 from rhodecode.lib.caching_query import FromCache
50
50
51 from rhodecode.model import meta
51 from rhodecode.model import meta
52 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
52 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
53 UserLog, RepoGroup, RhodeCodeSetting
53 UserLog, RepoGroup, RhodeCodeSetting
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def recursive_replace(str_, replace=' '):
59 def recursive_replace(str_, replace=' '):
60 """Recursive replace of given sign to just one instance
60 """Recursive replace of given sign to just one instance
61
61
62 :param str_: given string
62 :param str_: given string
63 :param replace: char to find and replace multiple instances
63 :param replace: char to find and replace multiple instances
64
64
65 Examples::
65 Examples::
66 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
66 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
67 'Mighty-Mighty-Bo-sstones'
67 'Mighty-Mighty-Bo-sstones'
68 """
68 """
69
69
70 if str_.find(replace * 2) == -1:
70 if str_.find(replace * 2) == -1:
71 return str_
71 return str_
72 else:
72 else:
73 str_ = str_.replace(replace * 2, replace)
73 str_ = str_.replace(replace * 2, replace)
74 return recursive_replace(str_, replace)
74 return recursive_replace(str_, replace)
75
75
76
76
77 def repo_name_slug(value):
77 def repo_name_slug(value):
78 """Return slug of name of repository
78 """Return slug of name of repository
79 This function is called on each creation/modification
79 This function is called on each creation/modification
80 of repository to prevent bad names in repo
80 of repository to prevent bad names in repo
81 """
81 """
82
82
83 slug = remove_formatting(value)
83 slug = remove_formatting(value)
84 slug = strip_tags(slug)
84 slug = strip_tags(slug)
85
85
86 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
86 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
87 slug = slug.replace(c, '-')
87 slug = slug.replace(c, '-')
88 slug = recursive_replace(slug, '-')
88 slug = recursive_replace(slug, '-')
89 slug = collapse(slug, '-')
89 slug = collapse(slug, '-')
90 return slug
90 return slug
91
91
92
92
93 def get_repo_slug(request):
93 def get_repo_slug(request):
94 return request.environ['pylons.routes_dict'].get('repo_name')
94 return request.environ['pylons.routes_dict'].get('repo_name')
95
95
96
96
97 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
97 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
98 """
98 """
99 Action logger for various actions made by users
99 Action logger for various actions made by users
100
100
101 :param user: user that made this action, can be a unique username string or
101 :param user: user that made this action, can be a unique username string or
102 object containing user_id attribute
102 object containing user_id attribute
103 :param action: action to log, should be on of predefined unique actions for
103 :param action: action to log, should be on of predefined unique actions for
104 easy translations
104 easy translations
105 :param repo: string name of repository or object containing repo_id,
105 :param repo: string name of repository or object containing repo_id,
106 that action was made on
106 that action was made on
107 :param ipaddr: optional ip address from what the action was made
107 :param ipaddr: optional ip address from what the action was made
108 :param sa: optional sqlalchemy session
108 :param sa: optional sqlalchemy session
109
109
110 """
110 """
111
111
112 if not sa:
112 if not sa:
113 sa = meta.Session
113 sa = meta.Session
114
114
115 try:
115 try:
116 if hasattr(user, 'user_id'):
116 if hasattr(user, 'user_id'):
117 user_obj = user
117 user_obj = user
118 elif isinstance(user, basestring):
118 elif isinstance(user, basestring):
119 user_obj = User.get_by_username(user)
119 user_obj = User.get_by_username(user)
120 else:
120 else:
121 raise Exception('You have to provide user object or username')
121 raise Exception('You have to provide user object or username')
122
122
123 if hasattr(repo, 'repo_id'):
123 if hasattr(repo, 'repo_id'):
124 repo_obj = Repository.get(repo.repo_id)
124 repo_obj = Repository.get(repo.repo_id)
125 repo_name = repo_obj.repo_name
125 repo_name = repo_obj.repo_name
126 elif isinstance(repo, basestring):
126 elif isinstance(repo, basestring):
127 repo_name = repo.lstrip('/')
127 repo_name = repo.lstrip('/')
128 repo_obj = Repository.get_by_repo_name(repo_name)
128 repo_obj = Repository.get_by_repo_name(repo_name)
129 else:
129 else:
130 raise Exception('You have to provide repository to action logger')
130 raise Exception('You have to provide repository to action logger')
131
131
132 user_log = UserLog()
132 user_log = UserLog()
133 user_log.user_id = user_obj.user_id
133 user_log.user_id = user_obj.user_id
134 user_log.action = action
134 user_log.action = action
135
135
136 user_log.repository_id = repo_obj.repo_id
136 user_log.repository_id = repo_obj.repo_id
137 user_log.repository_name = repo_name
137 user_log.repository_name = repo_name
138
138
139 user_log.action_date = datetime.datetime.now()
139 user_log.action_date = datetime.datetime.now()
140 user_log.user_ip = ipaddr
140 user_log.user_ip = ipaddr
141 sa.add(user_log)
141 sa.add(user_log)
142
142
143 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
143 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
144 if commit:
144 if commit:
145 sa.commit()
145 sa.commit()
146 except:
146 except:
147 log.error(traceback.format_exc())
147 log.error(traceback.format_exc())
148 raise
148 raise
149
149
150
150
151 def get_repos(path, recursive=False):
151 def get_repos(path, recursive=False):
152 """
152 """
153 Scans given path for repos and return (name,(type,path)) tuple
153 Scans given path for repos and return (name,(type,path)) tuple
154
154
155 :param path: path to scann for repositories
155 :param path: path to scan for repositories
156 :param recursive: recursive search and return names with subdirs in front
156 :param recursive: recursive search and return names with subdirs in front
157 """
157 """
158
158
159 # remove ending slash for better results
159 # remove ending slash for better results
160 path = path.rstrip('/')
160 path = path.rstrip(os.sep)
161
161
162 def _get_repos(p):
162 def _get_repos(p):
163 if not os.access(p, os.W_OK):
163 if not os.access(p, os.W_OK):
164 return
164 return
165 for dirpath in os.listdir(p):
165 for dirpath in os.listdir(p):
166 if os.path.isfile(os.path.join(p, dirpath)):
166 if os.path.isfile(os.path.join(p, dirpath)):
167 continue
167 continue
168 cur_path = os.path.join(p, dirpath)
168 cur_path = os.path.join(p, dirpath)
169 try:
169 try:
170 scm_info = get_scm(cur_path)
170 scm_info = get_scm(cur_path)
171 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
171 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
172 except VCSError:
172 except VCSError:
173 if not recursive:
173 if not recursive:
174 continue
174 continue
175 #check if this dir containts other repos for recursive scan
175 #check if this dir containts other repos for recursive scan
176 rec_path = os.path.join(p, dirpath)
176 rec_path = os.path.join(p, dirpath)
177 if os.path.isdir(rec_path):
177 if os.path.isdir(rec_path):
178 for inner_scm in _get_repos(rec_path):
178 for inner_scm in _get_repos(rec_path):
179 yield inner_scm
179 yield inner_scm
180
180
181 return _get_repos(path)
181 return _get_repos(path)
182
182
183
183
184 def is_valid_repo(repo_name, base_path):
184 def is_valid_repo(repo_name, base_path):
185 """
185 """
186 Returns True if given path is a valid repository False otherwise
186 Returns True if given path is a valid repository False otherwise
187 :param repo_name:
187 :param repo_name:
188 :param base_path:
188 :param base_path:
189
189
190 :return True: if given path is a valid repository
190 :return True: if given path is a valid repository
191 """
191 """
192 full_path = os.path.join(base_path, repo_name)
192 full_path = os.path.join(base_path, repo_name)
193
193
194 try:
194 try:
195 get_scm(full_path)
195 get_scm(full_path)
196 return True
196 return True
197 except VCSError:
197 except VCSError:
198 return False
198 return False
199
199
200 def is_valid_repos_group(repos_group_name, base_path):
200 def is_valid_repos_group(repos_group_name, base_path):
201 """
201 """
202 Returns True if given path is a repos group False otherwise
202 Returns True if given path is a repos group False otherwise
203
203
204 :param repo_name:
204 :param repo_name:
205 :param base_path:
205 :param base_path:
206 """
206 """
207 full_path = os.path.join(base_path, repos_group_name)
207 full_path = os.path.join(base_path, repos_group_name)
208
208
209 # check if it's not a repo
209 # check if it's not a repo
210 if is_valid_repo(repos_group_name, base_path):
210 if is_valid_repo(repos_group_name, base_path):
211 return False
211 return False
212
212
213 # check if it's a valid path
213 # check if it's a valid path
214 if os.path.isdir(full_path):
214 if os.path.isdir(full_path):
215 return True
215 return True
216
216
217 return False
217 return False
218
218
219 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
219 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
220 while True:
220 while True:
221 ok = raw_input(prompt)
221 ok = raw_input(prompt)
222 if ok in ('y', 'ye', 'yes'):
222 if ok in ('y', 'ye', 'yes'):
223 return True
223 return True
224 if ok in ('n', 'no', 'nop', 'nope'):
224 if ok in ('n', 'no', 'nop', 'nope'):
225 return False
225 return False
226 retries = retries - 1
226 retries = retries - 1
227 if retries < 0:
227 if retries < 0:
228 raise IOError
228 raise IOError
229 print complaint
229 print complaint
230
230
231 #propagated from mercurial documentation
231 #propagated from mercurial documentation
232 ui_sections = ['alias', 'auth',
232 ui_sections = ['alias', 'auth',
233 'decode/encode', 'defaults',
233 'decode/encode', 'defaults',
234 'diff', 'email',
234 'diff', 'email',
235 'extensions', 'format',
235 'extensions', 'format',
236 'merge-patterns', 'merge-tools',
236 'merge-patterns', 'merge-tools',
237 'hooks', 'http_proxy',
237 'hooks', 'http_proxy',
238 'smtp', 'patch',
238 'smtp', 'patch',
239 'paths', 'profiling',
239 'paths', 'profiling',
240 'server', 'trusted',
240 'server', 'trusted',
241 'ui', 'web', ]
241 'ui', 'web', ]
242
242
243
243
244 def make_ui(read_from='file', path=None, checkpaths=True):
244 def make_ui(read_from='file', path=None, checkpaths=True):
245 """A function that will read python rc files or database
245 """A function that will read python rc files or database
246 and make an mercurial ui object from read options
246 and make an mercurial ui object from read options
247
247
248 :param path: path to mercurial config file
248 :param path: path to mercurial config file
249 :param checkpaths: check the path
249 :param checkpaths: check the path
250 :param read_from: read from 'file' or 'db'
250 :param read_from: read from 'file' or 'db'
251 """
251 """
252
252
253 baseui = ui.ui()
253 baseui = ui.ui()
254
254
255 #clean the baseui object
255 #clean the baseui object
256 baseui._ocfg = config.config()
256 baseui._ocfg = config.config()
257 baseui._ucfg = config.config()
257 baseui._ucfg = config.config()
258 baseui._tcfg = config.config()
258 baseui._tcfg = config.config()
259
259
260 if read_from == 'file':
260 if read_from == 'file':
261 if not os.path.isfile(path):
261 if not os.path.isfile(path):
262 log.warning('Unable to read config file %s' % path)
262 log.warning('Unable to read config file %s' % path)
263 return False
263 return False
264 log.debug('reading hgrc from %s', path)
264 log.debug('reading hgrc from %s', path)
265 cfg = config.config()
265 cfg = config.config()
266 cfg.read(path)
266 cfg.read(path)
267 for section in ui_sections:
267 for section in ui_sections:
268 for k, v in cfg.items(section):
268 for k, v in cfg.items(section):
269 log.debug('settings ui from file[%s]%s:%s', section, k, v)
269 log.debug('settings ui from file[%s]%s:%s', section, k, v)
270 baseui.setconfig(section, k, v)
270 baseui.setconfig(section, k, v)
271
271
272 elif read_from == 'db':
272 elif read_from == 'db':
273 sa = meta.Session
273 sa = meta.Session
274 ret = sa.query(RhodeCodeUi)\
274 ret = sa.query(RhodeCodeUi)\
275 .options(FromCache("sql_cache_short",
275 .options(FromCache("sql_cache_short",
276 "get_hg_ui_settings")).all()
276 "get_hg_ui_settings")).all()
277
277
278 hg_ui = ret
278 hg_ui = ret
279 for ui_ in hg_ui:
279 for ui_ in hg_ui:
280 if ui_.ui_active:
280 if ui_.ui_active:
281 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
281 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
282 ui_.ui_key, ui_.ui_value)
282 ui_.ui_key, ui_.ui_value)
283 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
283 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
284
284
285 meta.Session.remove()
285 meta.Session.remove()
286 return baseui
286 return baseui
287
287
288
288
289 def set_rhodecode_config(config):
289 def set_rhodecode_config(config):
290 """
290 """
291 Updates pylons config with new settings from database
291 Updates pylons config with new settings from database
292
292
293 :param config:
293 :param config:
294 """
294 """
295 hgsettings = RhodeCodeSetting.get_app_settings()
295 hgsettings = RhodeCodeSetting.get_app_settings()
296
296
297 for k, v in hgsettings.items():
297 for k, v in hgsettings.items():
298 config[k] = v
298 config[k] = v
299
299
300
300
301 def invalidate_cache(cache_key, *args):
301 def invalidate_cache(cache_key, *args):
302 """
302 """
303 Puts cache invalidation task into db for
303 Puts cache invalidation task into db for
304 further global cache invalidation
304 further global cache invalidation
305 """
305 """
306
306
307 from rhodecode.model.scm import ScmModel
307 from rhodecode.model.scm import ScmModel
308
308
309 if cache_key.startswith('get_repo_cached_'):
309 if cache_key.startswith('get_repo_cached_'):
310 name = cache_key.split('get_repo_cached_')[-1]
310 name = cache_key.split('get_repo_cached_')[-1]
311 ScmModel().mark_for_invalidation(name)
311 ScmModel().mark_for_invalidation(name)
312
312
313
313
314 class EmptyChangeset(BaseChangeset):
314 class EmptyChangeset(BaseChangeset):
315 """
315 """
316 An dummy empty changeset. It's possible to pass hash when creating
316 An dummy empty changeset. It's possible to pass hash when creating
317 an EmptyChangeset
317 an EmptyChangeset
318 """
318 """
319
319
320 def __init__(self, cs='0' * 40, repo=None, requested_revision=None, alias=None):
320 def __init__(self, cs='0' * 40, repo=None, requested_revision=None, alias=None):
321 self._empty_cs = cs
321 self._empty_cs = cs
322 self.revision = -1
322 self.revision = -1
323 self.message = ''
323 self.message = ''
324 self.author = ''
324 self.author = ''
325 self.date = ''
325 self.date = ''
326 self.repository = repo
326 self.repository = repo
327 self.requested_revision = requested_revision
327 self.requested_revision = requested_revision
328 self.alias = alias
328 self.alias = alias
329
329
330 @LazyProperty
330 @LazyProperty
331 def raw_id(self):
331 def raw_id(self):
332 """
332 """
333 Returns raw string identifying this changeset, useful for web
333 Returns raw string identifying this changeset, useful for web
334 representation.
334 representation.
335 """
335 """
336
336
337 return self._empty_cs
337 return self._empty_cs
338
338
339 @LazyProperty
339 @LazyProperty
340 def branch(self):
340 def branch(self):
341 return get_backend(self.alias).DEFAULT_BRANCH_NAME
341 return get_backend(self.alias).DEFAULT_BRANCH_NAME
342
342
343 @LazyProperty
343 @LazyProperty
344 def short_id(self):
344 def short_id(self):
345 return self.raw_id[:12]
345 return self.raw_id[:12]
346
346
347 def get_file_changeset(self, path):
347 def get_file_changeset(self, path):
348 return self
348 return self
349
349
350 def get_file_content(self, path):
350 def get_file_content(self, path):
351 return u''
351 return u''
352
352
353 def get_file_size(self, path):
353 def get_file_size(self, path):
354 return 0
354 return 0
355
355
356
356
357 def map_groups(groups):
357 def map_groups(groups):
358 """
358 """
359 Checks for groups existence, and creates groups structures.
359 Checks for groups existence, and creates groups structures.
360 It returns last group in structure
360 It returns last group in structure
361
361
362 :param groups: list of groups structure
362 :param groups: list of groups structure
363 """
363 """
364 sa = meta.Session
364 sa = meta.Session
365
365
366 parent = None
366 parent = None
367 group = None
367 group = None
368
368
369 # last element is repo in nested groups structure
369 # last element is repo in nested groups structure
370 groups = groups[:-1]
370 groups = groups[:-1]
371
371
372 for lvl, group_name in enumerate(groups):
372 for lvl, group_name in enumerate(groups):
373 group_name = '/'.join(groups[:lvl] + [group_name])
373 group_name = '/'.join(groups[:lvl] + [group_name])
374 group = sa.query(RepoGroup).filter(RepoGroup.group_name == group_name).scalar()
374 group = sa.query(RepoGroup).filter(RepoGroup.group_name == group_name).scalar()
375
375
376 if group is None:
376 if group is None:
377 group = RepoGroup(group_name, parent)
377 group = RepoGroup(group_name, parent)
378 sa.add(group)
378 sa.add(group)
379 sa.commit()
379 sa.commit()
380 parent = group
380 parent = group
381 return group
381 return group
382
382
383
383
384 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
384 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
385 """
385 """
386 maps all repos given in initial_repo_list, non existing repositories
386 maps all repos given in initial_repo_list, non existing repositories
387 are created, if remove_obsolete is True it also check for db entries
387 are created, if remove_obsolete is True it also check for db entries
388 that are not in initial_repo_list and removes them.
388 that are not in initial_repo_list and removes them.
389
389
390 :param initial_repo_list: list of repositories found by scanning methods
390 :param initial_repo_list: list of repositories found by scanning methods
391 :param remove_obsolete: check for obsolete entries in database
391 :param remove_obsolete: check for obsolete entries in database
392 """
392 """
393 from rhodecode.model.repo import RepoModel
393 from rhodecode.model.repo import RepoModel
394 sa = meta.Session
394 sa = meta.Session
395 rm = RepoModel()
395 rm = RepoModel()
396 user = sa.query(User).filter(User.admin == True).first()
396 user = sa.query(User).filter(User.admin == True).first()
397 if user is None:
397 if user is None:
398 raise Exception('Missing administrative account !')
398 raise Exception('Missing administrative account !')
399 added = []
399 added = []
400
400
401 for name, repo in initial_repo_list.items():
401 for name, repo in initial_repo_list.items():
402 group = map_groups(name.split(Repository.url_sep()))
402 group = map_groups(name.split(Repository.url_sep()))
403 if not rm.get_by_repo_name(name, cache=False):
403 if not rm.get_by_repo_name(name, cache=False):
404 log.info('repository %s not found creating default', name)
404 log.info('repository %s not found creating default', name)
405 added.append(name)
405 added.append(name)
406 form_data = {
406 form_data = {
407 'repo_name': name,
407 'repo_name': name,
408 'repo_name_full': name,
408 'repo_name_full': name,
409 'repo_type': repo.alias,
409 'repo_type': repo.alias,
410 'description': repo.description \
410 'description': repo.description \
411 if repo.description != 'unknown' else \
411 if repo.description != 'unknown' else \
412 '%s repository' % name,
412 '%s repository' % name,
413 'private': False,
413 'private': False,
414 'group_id': getattr(group, 'group_id', None)
414 'group_id': getattr(group, 'group_id', None)
415 }
415 }
416 rm.create(form_data, user, just_db=True)
416 rm.create(form_data, user, just_db=True)
417 sa.commit()
417 sa.commit()
418 removed = []
418 removed = []
419 if remove_obsolete:
419 if remove_obsolete:
420 #remove from database those repositories that are not in the filesystem
420 #remove from database those repositories that are not in the filesystem
421 for repo in sa.query(Repository).all():
421 for repo in sa.query(Repository).all():
422 if repo.repo_name not in initial_repo_list.keys():
422 if repo.repo_name not in initial_repo_list.keys():
423 removed.append(repo.repo_name)
423 removed.append(repo.repo_name)
424 sa.delete(repo)
424 sa.delete(repo)
425 sa.commit()
425 sa.commit()
426
426
427 return added, removed
427 return added, removed
428
428
429 # set cache regions for beaker so celery can utilise it
429 # set cache regions for beaker so celery can utilise it
430 def add_cache(settings):
430 def add_cache(settings):
431 cache_settings = {'regions': None}
431 cache_settings = {'regions': None}
432 for key in settings.keys():
432 for key in settings.keys():
433 for prefix in ['beaker.cache.', 'cache.']:
433 for prefix in ['beaker.cache.', 'cache.']:
434 if key.startswith(prefix):
434 if key.startswith(prefix):
435 name = key.split(prefix)[1].strip()
435 name = key.split(prefix)[1].strip()
436 cache_settings[name] = settings[key].strip()
436 cache_settings[name] = settings[key].strip()
437 if cache_settings['regions']:
437 if cache_settings['regions']:
438 for region in cache_settings['regions'].split(','):
438 for region in cache_settings['regions'].split(','):
439 region = region.strip()
439 region = region.strip()
440 region_settings = {}
440 region_settings = {}
441 for key, value in cache_settings.items():
441 for key, value in cache_settings.items():
442 if key.startswith(region):
442 if key.startswith(region):
443 region_settings[key.split('.')[1]] = value
443 region_settings[key.split('.')[1]] = value
444 region_settings['expire'] = int(region_settings.get('expire',
444 region_settings['expire'] = int(region_settings.get('expire',
445 60))
445 60))
446 region_settings.setdefault('lock_dir',
446 region_settings.setdefault('lock_dir',
447 cache_settings.get('lock_dir'))
447 cache_settings.get('lock_dir'))
448 region_settings.setdefault('data_dir',
448 region_settings.setdefault('data_dir',
449 cache_settings.get('data_dir'))
449 cache_settings.get('data_dir'))
450
450
451 if 'type' not in region_settings:
451 if 'type' not in region_settings:
452 region_settings['type'] = cache_settings.get('type',
452 region_settings['type'] = cache_settings.get('type',
453 'memory')
453 'memory')
454 beaker.cache.cache_regions[region] = region_settings
454 beaker.cache.cache_regions[region] = region_settings
455
455
456
456
457 #==============================================================================
457 #==============================================================================
458 # TEST FUNCTIONS AND CREATORS
458 # TEST FUNCTIONS AND CREATORS
459 #==============================================================================
459 #==============================================================================
460 def create_test_index(repo_location, config, full_index):
460 def create_test_index(repo_location, config, full_index):
461 """
461 """
462 Makes default test index
462 Makes default test index
463
463
464 :param config: test config
464 :param config: test config
465 :param full_index:
465 :param full_index:
466 """
466 """
467
467
468 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
468 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
469 from rhodecode.lib.pidlock import DaemonLock, LockHeld
469 from rhodecode.lib.pidlock import DaemonLock, LockHeld
470
470
471 repo_location = repo_location
471 repo_location = repo_location
472
472
473 index_location = os.path.join(config['app_conf']['index_dir'])
473 index_location = os.path.join(config['app_conf']['index_dir'])
474 if not os.path.exists(index_location):
474 if not os.path.exists(index_location):
475 os.makedirs(index_location)
475 os.makedirs(index_location)
476
476
477 try:
477 try:
478 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
478 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
479 WhooshIndexingDaemon(index_location=index_location,
479 WhooshIndexingDaemon(index_location=index_location,
480 repo_location=repo_location)\
480 repo_location=repo_location)\
481 .run(full_index=full_index)
481 .run(full_index=full_index)
482 l.release()
482 l.release()
483 except LockHeld:
483 except LockHeld:
484 pass
484 pass
485
485
486
486
487 def create_test_env(repos_test_path, config):
487 def create_test_env(repos_test_path, config):
488 """
488 """
489 Makes a fresh database and
489 Makes a fresh database and
490 install test repository into tmp dir
490 install test repository into tmp dir
491 """
491 """
492 from rhodecode.lib.db_manage import DbManage
492 from rhodecode.lib.db_manage import DbManage
493 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
493 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
494
494
495 # PART ONE create db
495 # PART ONE create db
496 dbconf = config['sqlalchemy.db1.url']
496 dbconf = config['sqlalchemy.db1.url']
497 log.debug('making test db %s', dbconf)
497 log.debug('making test db %s', dbconf)
498
498
499 # create test dir if it doesn't exist
499 # create test dir if it doesn't exist
500 if not os.path.isdir(repos_test_path):
500 if not os.path.isdir(repos_test_path):
501 log.debug('Creating testdir %s' % repos_test_path)
501 log.debug('Creating testdir %s' % repos_test_path)
502 os.makedirs(repos_test_path)
502 os.makedirs(repos_test_path)
503
503
504 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
504 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
505 tests=True)
505 tests=True)
506 dbmanage.create_tables(override=True)
506 dbmanage.create_tables(override=True)
507 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
507 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
508 dbmanage.create_default_user()
508 dbmanage.create_default_user()
509 dbmanage.admin_prompt()
509 dbmanage.admin_prompt()
510 dbmanage.create_permissions()
510 dbmanage.create_permissions()
511 dbmanage.populate_default_permissions()
511 dbmanage.populate_default_permissions()
512 Session.commit()
512 Session.commit()
513 # PART TWO make test repo
513 # PART TWO make test repo
514 log.debug('making test vcs repositories')
514 log.debug('making test vcs repositories')
515
515
516 idx_path = config['app_conf']['index_dir']
516 idx_path = config['app_conf']['index_dir']
517 data_path = config['app_conf']['cache_dir']
517 data_path = config['app_conf']['cache_dir']
518
518
519 #clean index and data
519 #clean index and data
520 if idx_path and os.path.exists(idx_path):
520 if idx_path and os.path.exists(idx_path):
521 log.debug('remove %s' % idx_path)
521 log.debug('remove %s' % idx_path)
522 shutil.rmtree(idx_path)
522 shutil.rmtree(idx_path)
523
523
524 if data_path and os.path.exists(data_path):
524 if data_path and os.path.exists(data_path):
525 log.debug('remove %s' % data_path)
525 log.debug('remove %s' % data_path)
526 shutil.rmtree(data_path)
526 shutil.rmtree(data_path)
527
527
528 #CREATE DEFAULT HG REPOSITORY
528 #CREATE DEFAULT HG REPOSITORY
529 cur_dir = dn(dn(abspath(__file__)))
529 cur_dir = dn(dn(abspath(__file__)))
530 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
530 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
531 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
531 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
532 tar.close()
532 tar.close()
533
533
534
534
535 #==============================================================================
535 #==============================================================================
536 # PASTER COMMANDS
536 # PASTER COMMANDS
537 #==============================================================================
537 #==============================================================================
538 class BasePasterCommand(Command):
538 class BasePasterCommand(Command):
539 """
539 """
540 Abstract Base Class for paster commands.
540 Abstract Base Class for paster commands.
541
541
542 The celery commands are somewhat aggressive about loading
542 The celery commands are somewhat aggressive about loading
543 celery.conf, and since our module sets the `CELERY_LOADER`
543 celery.conf, and since our module sets the `CELERY_LOADER`
544 environment variable to our loader, we have to bootstrap a bit and
544 environment variable to our loader, we have to bootstrap a bit and
545 make sure we've had a chance to load the pylons config off of the
545 make sure we've had a chance to load the pylons config off of the
546 command line, otherwise everything fails.
546 command line, otherwise everything fails.
547 """
547 """
548 min_args = 1
548 min_args = 1
549 min_args_error = "Please provide a paster config file as an argument."
549 min_args_error = "Please provide a paster config file as an argument."
550 takes_config_file = 1
550 takes_config_file = 1
551 requires_config_file = True
551 requires_config_file = True
552
552
553 def notify_msg(self, msg, log=False):
553 def notify_msg(self, msg, log=False):
554 """Make a notification to user, additionally if logger is passed
554 """Make a notification to user, additionally if logger is passed
555 it logs this action using given logger
555 it logs this action using given logger
556
556
557 :param msg: message that will be printed to user
557 :param msg: message that will be printed to user
558 :param log: logging instance, to use to additionally log this message
558 :param log: logging instance, to use to additionally log this message
559
559
560 """
560 """
561 if log and isinstance(log, logging):
561 if log and isinstance(log, logging):
562 log(msg)
562 log(msg)
563
563
564 def run(self, args):
564 def run(self, args):
565 """
565 """
566 Overrides Command.run
566 Overrides Command.run
567
567
568 Checks for a config file argument and loads it.
568 Checks for a config file argument and loads it.
569 """
569 """
570 if len(args) < self.min_args:
570 if len(args) < self.min_args:
571 raise BadCommand(
571 raise BadCommand(
572 self.min_args_error % {'min_args': self.min_args,
572 self.min_args_error % {'min_args': self.min_args,
573 'actual_args': len(args)})
573 'actual_args': len(args)})
574
574
575 # Decrement because we're going to lob off the first argument.
575 # Decrement because we're going to lob off the first argument.
576 # @@ This is hacky
576 # @@ This is hacky
577 self.min_args -= 1
577 self.min_args -= 1
578 self.bootstrap_config(args[0])
578 self.bootstrap_config(args[0])
579 self.update_parser()
579 self.update_parser()
580 return super(BasePasterCommand, self).run(args[1:])
580 return super(BasePasterCommand, self).run(args[1:])
581
581
582 def update_parser(self):
582 def update_parser(self):
583 """
583 """
584 Abstract method. Allows for the class's parser to be updated
584 Abstract method. Allows for the class's parser to be updated
585 before the superclass's `run` method is called. Necessary to
585 before the superclass's `run` method is called. Necessary to
586 allow options/arguments to be passed through to the underlying
586 allow options/arguments to be passed through to the underlying
587 celery command.
587 celery command.
588 """
588 """
589 raise NotImplementedError("Abstract Method.")
589 raise NotImplementedError("Abstract Method.")
590
590
591 def bootstrap_config(self, conf):
591 def bootstrap_config(self, conf):
592 """
592 """
593 Loads the pylons configuration.
593 Loads the pylons configuration.
594 """
594 """
595 from pylons import config as pylonsconfig
595 from pylons import config as pylonsconfig
596
596
597 path_to_ini_file = os.path.realpath(conf)
597 path_to_ini_file = os.path.realpath(conf)
598 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
598 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
599 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
599 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,219 +1,219 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.notification
3 rhodecode.model.notification
4 ~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Model for notifications
6 Model for notifications
7
7
8
8
9 :created_on: Nov 20, 2011
9 :created_on: Nov 20, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import datetime
30 import datetime
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33
33
34 import rhodecode
34 import rhodecode
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.model import BaseModel
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import Notification, User, UserNotification
37 from rhodecode.model.db import Notification, User, UserNotification
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class NotificationModel(BaseModel):
42 class NotificationModel(BaseModel):
43
43
44 def __get_user(self, user):
44 def __get_user(self, user):
45 if isinstance(user, basestring):
45 if isinstance(user, basestring):
46 return User.get_by_username(username=user)
46 return User.get_by_username(username=user)
47 else:
47 else:
48 return self._get_instance(User, user)
48 return self._get_instance(User, user)
49
49
50 def __get_notification(self, notification):
50 def __get_notification(self, notification):
51 if isinstance(notification, Notification):
51 if isinstance(notification, Notification):
52 return notification
52 return notification
53 elif isinstance(notification, int):
53 elif isinstance(notification, int):
54 return Notification.get(notification)
54 return Notification.get(notification)
55 else:
55 else:
56 if notification:
56 if notification:
57 raise Exception('notification must be int or Instance'
57 raise Exception('notification must be int or Instance'
58 ' of Notification got %s' % type(notification))
58 ' of Notification got %s' % type(notification))
59
59
60 def create(self, created_by, subject, body, recipients=None,
60 def create(self, created_by, subject, body, recipients=None,
61 type_=Notification.TYPE_MESSAGE, with_email=True,
61 type_=Notification.TYPE_MESSAGE, with_email=True,
62 email_kwargs={}):
62 email_kwargs={}):
63 """
63 """
64
64
65 Creates notification of given type
65 Creates notification of given type
66
66
67 :param created_by: int, str or User instance. User who created this
67 :param created_by: int, str or User instance. User who created this
68 notification
68 notification
69 :param subject:
69 :param subject:
70 :param body:
70 :param body:
71 :param recipients: list of int, str or User objects, when None
71 :param recipients: list of int, str or User objects, when None
72 is given send to all admins
72 is given send to all admins
73 :param type_: type of notification
73 :param type_: type of notification
74 :param with_email: send email with this notification
74 :param with_email: send email with this notification
75 :param email_kwargs: additional dict to pass as args to email template
75 :param email_kwargs: additional dict to pass as args to email template
76 """
76 """
77 from rhodecode.lib.celerylib import tasks, run_task
77 from rhodecode.lib.celerylib import tasks, run_task
78
78
79 if recipients and not getattr(recipients, '__iter__', False):
79 if recipients and not getattr(recipients, '__iter__', False):
80 raise Exception('recipients must be a list of iterable')
80 raise Exception('recipients must be a list of iterable')
81
81
82 created_by_obj = self.__get_user(created_by)
82 created_by_obj = self.__get_user(created_by)
83
83
84 if recipients:
84 if recipients:
85 recipients_objs = []
85 recipients_objs = []
86 for u in recipients:
86 for u in recipients:
87 obj = self.__get_user(u)
87 obj = self.__get_user(u)
88 if obj:
88 if obj:
89 recipients_objs.append(obj)
89 recipients_objs.append(obj)
90 recipients_objs = set(recipients_objs)
90 recipients_objs = set(recipients_objs)
91 else:
91 else:
92 # empty recipients means to all admins
92 # empty recipients means to all admins
93 recipients_objs = User.query().filter(User.admin == True).all()
93 recipients_objs = User.query().filter(User.admin == True).all()
94
94
95 notif = Notification.create(created_by=created_by_obj, subject=subject,
95 notif = Notification.create(created_by=created_by_obj, subject=subject,
96 body=body, recipients=recipients_objs,
96 body=body, recipients=recipients_objs,
97 type_=type_)
97 type_=type_)
98
98
99 if with_email is False:
99 if with_email is False:
100 return notif
100 return notif
101
101
102 # send email with notification
102 # send email with notification
103 for rec in recipients_objs:
103 for rec in recipients_objs:
104 email_subject = NotificationModel().make_description(notif, False)
104 email_subject = NotificationModel().make_description(notif, False)
105 type_ = type_
105 type_ = type_
106 email_body = body
106 email_body = body
107 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
107 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
108 kwargs.update(email_kwargs)
108 kwargs.update(email_kwargs)
109 email_body_html = EmailNotificationModel()\
109 email_body_html = EmailNotificationModel()\
110 .get_email_tmpl(type_, **kwargs)
110 .get_email_tmpl(type_, **kwargs)
111 run_task(tasks.send_email, rec.email, email_subject, email_body,
111 run_task(tasks.send_email, rec.email, email_subject, email_body,
112 email_body_html)
112 email_body_html)
113
113
114 return notif
114 return notif
115
115
116 def delete(self, user, notification):
116 def delete(self, user, notification):
117 # we don't want to remove actual notification just the assignment
117 # we don't want to remove actual notification just the assignment
118 try:
118 try:
119 notification = self.__get_notification(notification)
119 notification = self.__get_notification(notification)
120 user = self.__get_user(user)
120 user = self.__get_user(user)
121 if notification and user:
121 if notification and user:
122 obj = UserNotification.query()\
122 obj = UserNotification.query()\
123 .filter(UserNotification.user == user)\
123 .filter(UserNotification.user == user)\
124 .filter(UserNotification.notification
124 .filter(UserNotification.notification
125 == notification)\
125 == notification)\
126 .one()
126 .one()
127 self.sa.delete(obj)
127 self.sa.delete(obj)
128 return True
128 return True
129 except Exception:
129 except Exception:
130 log.error(traceback.format_exc())
130 log.error(traceback.format_exc())
131 raise
131 raise
132
132
133 def get_for_user(self, user):
133 def get_for_user(self, user):
134 user = self.__get_user(user)
134 user = self.__get_user(user)
135 return user.notifications
135 return user.notifications
136
136
137 def mark_all_read_for_user(self, user):
137 def mark_all_read_for_user(self, user):
138 user = self.__get_user(user)
138 user = self.__get_user(user)
139 UserNotification.query()\
139 UserNotification.query()\
140 .filter(UserNotification.read==False)\
140 .filter(UserNotification.read==False)\
141 .update({'read': True})
141 .update({'read': True})
142
142
143 def get_unread_cnt_for_user(self, user):
143 def get_unread_cnt_for_user(self, user):
144 user = self.__get_user(user)
144 user = self.__get_user(user)
145 return UserNotification.query()\
145 return UserNotification.query()\
146 .filter(UserNotification.read == False)\
146 .filter(UserNotification.read == False)\
147 .filter(UserNotification.user == user).count()
147 .filter(UserNotification.user == user).count()
148
148
149 def get_unread_for_user(self, user):
149 def get_unread_for_user(self, user):
150 user = self.__get_user(user)
150 user = self.__get_user(user)
151 return [x.notification for x in UserNotification.query()\
151 return [x.notification for x in UserNotification.query()\
152 .filter(UserNotification.read == False)\
152 .filter(UserNotification.read == False)\
153 .filter(UserNotification.user == user).all()]
153 .filter(UserNotification.user == user).all()]
154
154
155 def get_user_notification(self, user, notification):
155 def get_user_notification(self, user, notification):
156 user = self.__get_user(user)
156 user = self.__get_user(user)
157 notification = self.__get_notification(notification)
157 notification = self.__get_notification(notification)
158
158
159 return UserNotification.query()\
159 return UserNotification.query()\
160 .filter(UserNotification.notification == notification)\
160 .filter(UserNotification.notification == notification)\
161 .filter(UserNotification.user == user).scalar()
161 .filter(UserNotification.user == user).scalar()
162
162
163 def make_description(self, notification, show_age=True):
163 def make_description(self, notification, show_age=True):
164 """
164 """
165 Creates a human readable description based on properties
165 Creates a human readable description based on properties
166 of notification object
166 of notification object
167 """
167 """
168
168
169 _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
169 _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
170 notification.TYPE_MESSAGE:_('sent message'),
170 notification.TYPE_MESSAGE:_('sent message'),
171 notification.TYPE_MENTION:_('mentioned you'),
171 notification.TYPE_MENTION:_('mentioned you'),
172 notification.TYPE_REGISTRATION:_('registered in RhodeCode')}
172 notification.TYPE_REGISTRATION:_('registered in RhodeCode')}
173
173
174 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
174 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
175
175
176 tmpl = "%(user)s %(action)s %(when)s"
176 tmpl = "%(user)s %(action)s %(when)s"
177 if show_age:
177 if show_age:
178 when = h.age(notification.created_on)
178 when = h.age(notification.created_on)
179 else:
179 else:
180 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
180 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
181 when = DTF(notification.created_on)
181 when = DTF(notification.created_on)
182 data = dict(user=notification.created_by_user.username,
182 data = dict(user=notification.created_by_user.username,
183 action=_map[notification.type_],
183 action=_map[notification.type_],
184 when=when)
184 when=when)
185 return tmpl % data
185 return tmpl % data
186
186
187
187
188 class EmailNotificationModel(BaseModel):
188 class EmailNotificationModel(BaseModel):
189
189
190 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
190 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
191 TYPE_PASSWORD_RESET = 'passoword_link'
191 TYPE_PASSWORD_RESET = 'passoword_link'
192 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
192 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
193 TYPE_DEFAULT = 'default'
193 TYPE_DEFAULT = 'default'
194
194
195 def __init__(self):
195 def __init__(self):
196 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
196 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
197 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
197 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
198
198
199 self.email_types = {
199 self.email_types = {
200 self.TYPE_CHANGESET_COMMENT:'email_templates/changeset_comment.html',
200 self.TYPE_CHANGESET_COMMENT:'email_templates/changeset_comment.html',
201 self.TYPE_PASSWORD_RESET:'email_templates/password_reset.html',
201 self.TYPE_PASSWORD_RESET:'email_templates/password_reset.html',
202 self.TYPE_REGISTRATION:'email_templates/registration.html',
202 self.TYPE_REGISTRATION:'email_templates/registration.html',
203 self.TYPE_DEFAULT:'email_templates/default.html'
203 self.TYPE_DEFAULT:'email_templates/default.html'
204 }
204 }
205
205
206 def get_email_tmpl(self, type_, **kwargs):
206 def get_email_tmpl(self, type_, **kwargs):
207 """
207 """
208 return generated template for email based on given type
208 return generated template for email based on given type
209
209
210 :param type_:
210 :param type_:
211 """
211 """
212
212
213 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
213 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
214 email_template = self._tmpl_lookup.get_template(base)
214 email_template = self._tmpl_lookup.get_template(base)
215 # translator inject
215 # translator inject
216 _kwargs = {'_':_}
216 _kwargs = {'_':_}
217 _kwargs.update(kwargs)
217 _kwargs.update(kwargs)
218 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
218 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
219 return email_template.render(**_kwargs)
219 return email_template.render(**_kwargs)
@@ -1,192 +1,192 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.html"/>
3 <%inherit file="/base/base.html"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
7 </%def>
7 </%def>
8
8
9 <%def name="breadcrumbs_links()">
9 <%def name="breadcrumbs_links()">
10 ${h.link_to(u'Home',h.url('/'))}
10 ${h.link_to(u'Home',h.url('/'))}
11 &raquo;
11 &raquo;
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 &raquo;
13 &raquo;
14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
15 </%def>
15 </%def>
16
16
17 <%def name="page_nav()">
17 <%def name="page_nav()">
18 ${self.menu('changelog')}
18 ${self.menu('changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <div class="box">
22 <div class="box">
23 <!-- box / title -->
23 <!-- box / title -->
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 </div>
26 </div>
27 <div class="table">
27 <div class="table">
28 % if c.pagination:
28 % if c.pagination:
29 <div id="graph">
29 <div id="graph">
30 <div id="graph_nodes">
30 <div id="graph_nodes">
31 <canvas id="graph_canvas"></canvas>
31 <canvas id="graph_canvas"></canvas>
32 </div>
32 </div>
33 <div id="graph_content">
33 <div id="graph_content">
34 <div class="container_header">
34 <div class="container_header">
35 ${h.form(h.url.current(),method='get')}
35 ${h.form(h.url.current(),method='get')}
36 <div class="info_box" style="float:left">
36 <div class="info_box" style="float:left">
37 ${h.submit('set',_('Show'),class_="ui-btn")}
37 ${h.submit('set',_('Show'),class_="ui-btn")}
38 ${h.text('size',size=1,value=c.size)}
38 ${h.text('size',size=1,value=c.size)}
39 ${_('revisions')}
39 ${_('revisions')}
40 </div>
40 </div>
41 ${h.end_form()}
41 ${h.end_form()}
42 <div id="rev_range_container" style="display:none"></div>
42 <div id="rev_range_container" style="display:none"></div>
43 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
43 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
44 </div>
44 </div>
45
45
46 %for cnt,cs in enumerate(c.pagination):
46 %for cnt,cs in enumerate(c.pagination):
47 <div id="chg_${cnt+1}" class="container ${'tablerow1' if cnt%2==0 else 'tablerow2'}">
47 <div id="chg_${cnt+1}" class="container ${'tablerow1' if cnt%2==0 else 'tablerow2'}">
48 <div class="left">
48 <div class="left">
49 <div>
49 <div>
50 ${h.checkbox(cs.short_id,class_="changeset_range")}
50 ${h.checkbox(cs.short_id,class_="changeset_range")}
51 <span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span>
51 <span class="tooltip" title="${cs.date}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
52 </div>
52 </div>
53 <div class="author">
53 <div class="author">
54 <div class="gravatar">
54 <div class="gravatar">
55 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
55 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
56 </div>
56 </div>
57 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
57 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
58 </div>
58 </div>
59 <div class="date">${cs.date}</div>
59 <div class="date">${cs.date}</div>
60 </div>
60 </div>
61 <div class="mid">
61 <div class="mid">
62 <div class="message">${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
62 <div class="message">${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
63 </div>
63 </div>
64 <div class="right">
64 <div class="right">
65 <div id="${cs.raw_id}_changes_info" class="changes">
65 <div id="${cs.raw_id}_changes_info" class="changes">
66 <span id="${cs.raw_id}" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</span>
66 <span id="${cs.raw_id}" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</span>
67 </div>
67 </div>
68 %if cs.parents:
68 %if cs.parents:
69 %for p_cs in reversed(cs.parents):
69 %for p_cs in reversed(cs.parents):
70 <div class="parent">${_('Parent')}
70 <div class="parent">${_('Parent')}
71 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
71 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
72 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
72 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
73 </div>
73 </div>
74 %endfor
74 %endfor
75 %else:
75 %else:
76 <div class="parent">${_('No parents')}</div>
76 <div class="parent">${_('No parents')}</div>
77 %endif
77 %endif
78
78
79 <span class="logtags">
79 <span class="logtags">
80 %if len(cs.parents)>1:
80 %if len(cs.parents)>1:
81 <span class="merge">${_('merge')}</span>
81 <span class="merge">${_('merge')}</span>
82 %endif
82 %endif
83 %if cs.branch:
83 %if cs.branch:
84 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
84 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
85 ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
85 ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
86 %endif
86 %endif
87 %for tag in cs.tags:
87 %for tag in cs.tags:
88 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
88 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
89 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
89 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
90 %endfor
90 %endfor
91 </span>
91 </span>
92 </div>
92 </div>
93 </div>
93 </div>
94
94
95 %endfor
95 %endfor
96 <div class="pagination-wh pagination-left">
96 <div class="pagination-wh pagination-left">
97 ${c.pagination.pager('$link_previous ~2~ $link_next')}
97 ${c.pagination.pager('$link_previous ~2~ $link_next')}
98 </div>
98 </div>
99 </div>
99 </div>
100 </div>
100 </div>
101
101
102 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
102 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
103 <script type="text/javascript">
103 <script type="text/javascript">
104 YAHOO.util.Event.onDOMReady(function(){
104 YAHOO.util.Event.onDOMReady(function(){
105
105
106 //Monitor range checkboxes and build a link to changesets
106 //Monitor range checkboxes and build a link to changesets
107 //ranges
107 //ranges
108 var checkboxes = YUD.getElementsByClassName('changeset_range');
108 var checkboxes = YUD.getElementsByClassName('changeset_range');
109 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
109 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
110 YUE.on(checkboxes,'click',function(e){
110 YUE.on(checkboxes,'click',function(e){
111 var checked_checkboxes = [];
111 var checked_checkboxes = [];
112 for (pos in checkboxes){
112 for (pos in checkboxes){
113 if(checkboxes[pos].checked){
113 if(checkboxes[pos].checked){
114 checked_checkboxes.push(checkboxes[pos]);
114 checked_checkboxes.push(checkboxes[pos]);
115 }
115 }
116 }
116 }
117 if(checked_checkboxes.length>1){
117 if(checked_checkboxes.length>1){
118 var rev_end = checked_checkboxes[0].name;
118 var rev_end = checked_checkboxes[0].name;
119 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
119 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
120
120
121 var url = url_tmpl.replace('__REVRANGE__',
121 var url = url_tmpl.replace('__REVRANGE__',
122 rev_start+'...'+rev_end);
122 rev_start+'...'+rev_end);
123
123
124 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
124 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
125 link = link.replace('__S',rev_start);
125 link = link.replace('__S',rev_start);
126 link = link.replace('__E',rev_end);
126 link = link.replace('__E',rev_end);
127 YUD.get('rev_range_container').innerHTML = link;
127 YUD.get('rev_range_container').innerHTML = link;
128 YUD.setStyle('rev_range_container','display','');
128 YUD.setStyle('rev_range_container','display','');
129 }
129 }
130 else{
130 else{
131 YUD.setStyle('rev_range_container','display','none');
131 YUD.setStyle('rev_range_container','display','none');
132
132
133 }
133 }
134 });
134 });
135
135
136 // Fetch changeset details
136 // Fetch changeset details
137 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
137 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
138 var id = e.currentTarget.id
138 var id = e.currentTarget.id
139 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
139 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
140 var url = url.replace('__CS__',id);
140 var url = url.replace('__CS__',id);
141 ypjax(url,id+'_changes_info',function(){tooltip_activate()});
141 ypjax(url,id+'_changes_info',function(){tooltip_activate()});
142 });
142 });
143
143
144 // change branch filter
144 // change branch filter
145 YUE.on(YUD.get('branch_filter'),'change',function(e){
145 YUE.on(YUD.get('branch_filter'),'change',function(e){
146 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
146 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
147 console.log(selected_branch);
147 console.log(selected_branch);
148 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
148 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
149 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
149 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
150 var url = url.replace('__BRANCH__',selected_branch);
150 var url = url.replace('__BRANCH__',selected_branch);
151 if(selected_branch != ''){
151 if(selected_branch != ''){
152 window.location = url;
152 window.location = url;
153 }else{
153 }else{
154 window.location = url_main;
154 window.location = url_main;
155 }
155 }
156
156
157 });
157 });
158
158
159 function set_canvas(heads) {
159 function set_canvas(heads) {
160 var c = document.getElementById('graph_nodes');
160 var c = document.getElementById('graph_nodes');
161 var t = document.getElementById('graph_content');
161 var t = document.getElementById('graph_content');
162 canvas = document.getElementById('graph_canvas');
162 canvas = document.getElementById('graph_canvas');
163 var div_h = t.clientHeight;
163 var div_h = t.clientHeight;
164 c.style.height=div_h+'px';
164 c.style.height=div_h+'px';
165 canvas.setAttribute('height',div_h);
165 canvas.setAttribute('height',div_h);
166 c.style.height=max_w+'px';
166 c.style.height=max_w+'px';
167 canvas.setAttribute('width',max_w);
167 canvas.setAttribute('width',max_w);
168 };
168 };
169 var heads = 1;
169 var heads = 1;
170 var max_heads = 0;
170 var max_heads = 0;
171 var jsdata = ${c.jsdata|n};
171 var jsdata = ${c.jsdata|n};
172
172
173 for( var i=0;i<jsdata.length;i++){
173 for( var i=0;i<jsdata.length;i++){
174 var m = Math.max.apply(Math, jsdata[i][1]);
174 var m = Math.max.apply(Math, jsdata[i][1]);
175 if (m>max_heads){
175 if (m>max_heads){
176 max_heads = m;
176 max_heads = m;
177 }
177 }
178 }
178 }
179 var max_w = Math.max(100,max_heads*25);
179 var max_w = Math.max(100,max_heads*25);
180 set_canvas(max_w);
180 set_canvas(max_w);
181
181
182 var r = new BranchRenderer();
182 var r = new BranchRenderer();
183 r.render(jsdata,max_w);
183 r.render(jsdata,max_w);
184
184
185 });
185 });
186 </script>
186 </script>
187 %else:
187 %else:
188 ${_('There are no changes yet')}
188 ${_('There are no changes yet')}
189 %endif
189 %endif
190 </div>
190 </div>
191 </div>
191 </div>
192 </%def> No newline at end of file
192 </%def>
@@ -1,189 +1,193 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.html"/>
3 <%inherit file="/base/base.html"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${c.repo_name} ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${c.rhodecode_name}
6 ${c.repo_name} ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${c.rhodecode_name}
7 </%def>
7 </%def>
8
8
9 <%def name="breadcrumbs_links()">
9 <%def name="breadcrumbs_links()">
10 ${h.link_to(u'Home',h.url('/'))}
10 ${h.link_to(u'Home',h.url('/'))}
11 &raquo;
11 &raquo;
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 &raquo;
13 &raquo;
14 ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}
14 ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}
15 </%def>
15 </%def>
16
16
17 <%def name="page_nav()">
17 <%def name="page_nav()">
18 ${self.menu('changelog')}
18 ${self.menu('changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <div class="box">
22 <div class="box">
23 <!-- box / title -->
23 <!-- box / title -->
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 </div>
26 </div>
27 <div class="table">
27 <div class="table">
28 <div class="diffblock">
28 <div class="diffblock">
29 <div class="code-header">
29 <div class="code-header">
30 <div class="date">${c.changeset.revision}:
30 <div class="date">${c.changeset.revision}:
31 ${h.link_to(h.short_id(c.changeset.raw_id),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
31 ${h.link_to(h.short_id(c.changeset.raw_id),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
32 ${c.changeset.date}</div>
32 ${c.changeset.date}</div>
33 <span class="diff-actions">
33 <span class="diff-actions">
34 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show')}" title="${_('raw diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
34 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show')}" title="${_('raw diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
35 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}" title="${_('download diff')}"><img class="icon" src="${h.url('/images/icons/down_16.png')}"/></a>
35 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}" title="${_('download diff')}"><img class="icon" src="${h.url('/images/icons/down_16.png')}"/></a>
36 ${c.ignorews_url()}
36 ${c.ignorews_url()}
37 ${c.context_url()}
37 ${c.context_url()}
38 </span>
38 </span>
39 <div class="comments-number" style="float:right;padding-right:5px">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
39 <div class="comments-number" style="float:right;padding-right:5px">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
40 </div>
40 </div>
41 </div>
41 </div>
42 <div id="changeset_content">
42 <div id="changeset_content">
43 <div class="container">
43 <div class="container">
44 <div class="left">
44 <div class="left">
45 <div class="author">
45 <div class="author">
46 <div class="gravatar">
46 <div class="gravatar">
47 <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/>
47 <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/>
48 </div>
48 </div>
49 <span>${h.person(c.changeset.author)}</span><br/>
49 <span>${h.person(c.changeset.author)}</span><br/>
50 <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
50 <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
51 </div>
51 </div>
52 <div class="message">${h.wrap_paragraphs(c.changeset.message)}</div>
52 <div class="message">${h.urlify_commit(h.wrap_paragraphs(c.changeset.message))}</div>
53 </div>
53 </div>
54 <div class="right">
54 <div class="right">
55 <div class="changes">
55 <div class="changes">
56 % if len(c.changeset.affected_files) <= c.affected_files_cut_off:
56 % if len(c.changeset.affected_files) <= c.affected_files_cut_off:
57 <span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
57 <span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
58 <span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
58 <span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
59 <span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
59 <span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
60 % else:
60 % else:
61 <span class="removed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
61 <span class="removed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
62 <span class="changed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
62 <span class="changed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
63 <span class="added" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
63 <span class="added" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
64 % endif
64 % endif
65 </div>
65 </div>
66
66
67 %if c.changeset.parents:
67 %if c.changeset.parents:
68 %for p_cs in reversed(c.changeset.parents):
68 %for p_cs in reversed(c.changeset.parents):
69 <div class="parent">${_('Parent')}
69 <div class="parent">${_('Parent')}
70 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
70 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
71 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
71 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
72 </div>
72 </div>
73 %endfor
73 %endfor
74 %else:
74 %else:
75 <div class="parent">${_('No parents')}</div>
75 <div class="parent">${_('No parents')}</div>
76 %endif
76 %endif
77 <span class="logtags">
77 <span class="logtags">
78 %if len(c.changeset.parents)>1:
78 %if len(c.changeset.parents)>1:
79 <span class="merge">${_('merge')}</span>
79 <span class="merge">${_('merge')}</span>
80 %endif
80 %endif
81 <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
81 <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
82 ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
82 ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
83 %for tag in c.changeset.tags:
83 %for tag in c.changeset.tags:
84 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
84 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
85 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
85 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
86 %endfor
86 %endfor
87 </span>
87 </span>
88 </div>
88 </div>
89 </div>
89 </div>
90 <span>
90 <span>
91 ${_('%s files affected with %s additions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
91 ${_('%s files affected with %s additions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
92 </span>
92 </span>
93 <div class="cs_files">
93 <div class="cs_files">
94 %for change,filenode,diff,cs1,cs2,stat in c.changes:
94 %for change,filenode,diff,cs1,cs2,stat in c.changes:
95 <div class="cs_${change}">
95 <div class="cs_${change}">
96 <div class="node">
96 <div class="node">
97 %if change != 'removed':
97 %if change != 'removed':
98 ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path))}
98 ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path)+"_target")}
99 %else:
99 %else:
100 ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))}
100 ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))}
101 %endif
101 %endif
102 </div>
102 </div>
103 <div class="changes">${h.fancy_file_stats(stat)}</div>
103 <div class="changes">${h.fancy_file_stats(stat)}</div>
104 </div>
104 </div>
105 %endfor
105 %endfor
106 % if c.cut_off:
106 % if c.cut_off:
107 ${_('Changeset was too big and was cut off...')}
107 ${_('Changeset was too big and was cut off...')}
108 % endif
108 % endif
109 </div>
109 </div>
110 </div>
110 </div>
111
111
112 </div>
112 </div>
113
113
114 ## diff block
114 ## diff block
115 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
115 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
116 ${diff_block.diff_block(c.changes)}
116 ${diff_block.diff_block(c.changes)}
117
117
118 ## template for inline comment form
118 ## template for inline comment form
119 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
119 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
120 ${comment.comment_inline_form(c.changeset)}
120 ${comment.comment_inline_form(c.changeset)}
121
121
122 ${comment.comments(c.changeset)}
122 ${comment.comments(c.changeset)}
123
123
124 <script type="text/javascript">
124 <script type="text/javascript">
125 var deleteComment = function(comment_id){
125 var deleteComment = function(comment_id){
126
126
127 var url = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}".replace('__COMMENT_ID__',comment_id);
127 var url = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}".replace('__COMMENT_ID__',comment_id);
128 var postData = '_method=delete';
128 var postData = '_method=delete';
129 var success = function(o){
129 var success = function(o){
130 var n = YUD.get('comment-'+comment_id);
130 var n = YUD.get('comment-'+comment_id);
131 n.parentNode.removeChild(n);
131 n.parentNode.removeChild(n);
132 }
132 }
133 ajaxPOST(url,postData,success);
133 ajaxPOST(url,postData,success);
134 }
134 }
135
135
136 YUE.onDOMReady(function(){
136 YUE.onDOMReady(function(){
137
137
138 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
138 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
139 var show = 'none';
139 var show = 'none';
140 var target = e.currentTarget;
140 var target = e.currentTarget;
141 console.log(target);
141 if(target.checked){
142 if(target.checked){
142 var show = ''
143 var show = ''
143 }
144 }
145 console.log('aa')
144 var boxid = YUD.getAttribute(target,'id_for');
146 var boxid = YUD.getAttribute(target,'id_for');
147 console.log(boxid);
145 var comments = YUQ('#{0} .inline-comments'.format(boxid));
148 var comments = YUQ('#{0} .inline-comments'.format(boxid));
149 console.log(comments)
146 for(c in comments){
150 for(c in comments){
147 YUD.setStyle(comments[c],'display',show);
151 YUD.setStyle(comments[c],'display',show);
148 }
152 }
149 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
153 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
150 for(c in btns){
154 for(c in btns){
151 YUD.setStyle(btns[c],'display',show);
155 YUD.setStyle(btns[c],'display',show);
152 }
156 }
153 })
157 })
154
158
155 YUE.on(YUQ('.line'),'click',function(e){
159 YUE.on(YUQ('.line'),'click',function(e){
156 var tr = e.currentTarget;
160 var tr = e.currentTarget;
157 injectInlineForm(tr);
161 injectInlineForm(tr);
158 });
162 });
159
163
160 // inject comments into they proper positions
164 // inject comments into they proper positions
161 var file_comments = YUQ('.inline-comment-placeholder');
165 var file_comments = YUQ('.inline-comment-placeholder');
162
166
163 for (f in file_comments){
167 for (f in file_comments){
164 var box = file_comments[f];
168 var box = file_comments[f];
165 var inlines = box.children;
169 var inlines = box.children;
166 for(var i=0; i<inlines.length; i++){
170 for(var i=0; i<inlines.length; i++){
167 try{
171 try{
168
172
169 var inline = inlines[i];
173 var inline = inlines[i];
170 var lineno = YUD.getAttribute(inlines[i],'line');
174 var lineno = YUD.getAttribute(inlines[i],'line');
171 var lineid = "{0}_{1}".format(YUD.getAttribute(inline,'target_id'),lineno);
175 var lineid = "{0}_{1}".format(YUD.getAttribute(inline,'target_id'),lineno);
172 var target_line = YUD.get(lineid);
176 var target_line = YUD.get(lineid);
173
177
174 var add = createInlineAddButton(target_line.parentNode,'${_("add another comment")}');
178 var add = createInlineAddButton(target_line.parentNode,'${_("add another comment")}');
175 YUD.insertAfter(add,target_line.parentNode);
179 YUD.insertAfter(add,target_line.parentNode);
176
180
177 var comment = new YAHOO.util.Element(tableTr('inline-comments',inline.innerHTML))
181 var comment = new YAHOO.util.Element(tableTr('inline-comments',inline.innerHTML))
178 YUD.insertAfter(comment,target_line.parentNode);
182 YUD.insertAfter(comment,target_line.parentNode);
179 }catch(e){
183 }catch(e){
180 console.log(e);
184 console.log(e);
181 }
185 }
182 }
186 }
183 }
187 }
184 })
188 })
185
189
186 </script>
190 </script>
187
191
188 </div>
192 </div>
189 </%def>
193 </%def>
@@ -1,89 +1,89 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${c.repo_name} ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)} - ${c.rhodecode_name}
5 ${c.repo_name} ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)} - ${c.rhodecode_name}
6 </%def>
6 </%def>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 ${h.link_to(u'Home',h.url('/'))}
9 ${h.link_to(u'Home',h.url('/'))}
10 &raquo;
10 &raquo;
11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 &raquo;
12 &raquo;
13 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
13 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
14 </%def>
14 </%def>
15
15
16 <%def name="page_nav()">
16 <%def name="page_nav()">
17 ${self.menu('changelog')}
17 ${self.menu('changelog')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21 <div class="box">
21 <div class="box">
22 <!-- box / title -->
22 <!-- box / title -->
23 <div class="title">
23 <div class="title">
24 ${self.breadcrumbs()}
24 ${self.breadcrumbs()}
25 </div>
25 </div>
26 <div class="table">
26 <div class="table">
27 <div id="body" class="diffblock">
27 <div id="body" class="diffblock">
28 <div class="code-header cv">
28 <div class="code-header cv">
29 <h3 class="code-header-title">${_('Compare View')}</h3>
29 <h3 class="code-header-title">${_('Compare View')}</h3>
30 <div>
30 <div>
31 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
31 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
32 </div>
32 </div>
33 </div>
33 </div>
34 </div>
34 </div>
35 <div id="changeset_compare_view_content">
35 <div id="changeset_compare_view_content">
36 <div class="container">
36 <div class="container">
37 <table class="compare_view_commits noborder">
37 <table class="compare_view_commits noborder">
38 %for cs in c.cs_ranges:
38 %for cs in c.cs_ranges:
39 <tr>
39 <tr>
40 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td>
40 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td>
41 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
41 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
42 <td><div class="author">${h.person(cs.author)}</div></td>
42 <td><div class="author">${h.person(cs.author)}</div></td>
43 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
43 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
44 <td><div class="message">${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div></td>
44 <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message))}</div></td>
45 </tr>
45 </tr>
46 %endfor
46 %endfor
47 </table>
47 </table>
48 </div>
48 </div>
49 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
49 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
50 <div class="cs_files">
50 <div class="cs_files">
51 %for cs in c.cs_ranges:
51 %for cs in c.cs_ranges:
52 <div class="cur_cs">r${cs}</div>
52 <div class="cur_cs">r${cs}</div>
53 %for change,filenode,diff,cs1,cs2,st in c.changes[cs.raw_id]:
53 %for change,filenode,diff,cs1,cs2,st in c.changes[cs.raw_id]:
54 <div class="cs_${change}">${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID(cs.raw_id,filenode.path)))}</div>
54 <div class="cs_${change}">${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID(cs.raw_id,filenode.path)))}</div>
55 %endfor
55 %endfor
56 %endfor
56 %endfor
57 </div>
57 </div>
58 </div>
58 </div>
59
59
60 </div>
60 </div>
61 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
61 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
62 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
62 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
63 %for cs in c.cs_ranges:
63 %for cs in c.cs_ranges:
64 ##${comment.comment_inline_form(cs)}
64 ##${comment.comment_inline_form(cs)}
65 ## diff block
65 ## diff block
66 <h3 style="border:none;padding-top:8px;">${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}</h3>
66 <h3 style="border:none;padding-top:8px;">${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}</h3>
67 ${diff_block.diff_block(c.changes[cs.raw_id])}
67 ${diff_block.diff_block(c.changes[cs.raw_id])}
68 ##${comment.comments(cs)}
68 ##${comment.comments(cs)}
69
69
70 %endfor
70 %endfor
71 <script type="text/javascript">
71 <script type="text/javascript">
72
72
73 YUE.onDOMReady(function(){
73 YUE.onDOMReady(function(){
74
74
75 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
75 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
76 var act = e.currentTarget.nextElementSibling;
76 var act = e.currentTarget.nextElementSibling;
77
77
78 if(YUD.hasClass(act,'active')){
78 if(YUD.hasClass(act,'active')){
79 YUD.removeClass(act,'active');
79 YUD.removeClass(act,'active');
80 YUD.setStyle(act,'display','none');
80 YUD.setStyle(act,'display','none');
81 }else{
81 }else{
82 YUD.addClass(act,'active');
82 YUD.addClass(act,'active');
83 YUD.setStyle(act,'display','');
83 YUD.setStyle(act,'display','');
84 }
84 }
85 });
85 });
86 })
86 })
87 </script>
87 </script>
88 </div>
88 </div>
89 </%def> No newline at end of file
89 </%def>
@@ -1,41 +1,41 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ##usage:
2 ##usage:
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 ## ${diff_block.diff_block(changes)}
4 ## ${diff_block.diff_block(changes)}
5 ##
5 ##
6 <%def name="diff_block(changes)">
6 <%def name="diff_block(changes)">
7
7
8 %for change,filenode,diff,cs1,cs2,stat in changes:
8 %for change,filenode,diff,cs1,cs2,stat in changes:
9 %if change !='removed':
9 %if change !='removed':
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" style="clear:both;height:90px;margin-top:-60px"></div>
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}_target" style="clear:both;height:90px;margin-top:-60px"></div>
11 <div class="diffblock margined comm">
11 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" class="diffblock margined comm">
12 <div class="code-header">
12 <div class="code-header">
13 <div class="changeset_header">
13 <div class="changeset_header">
14 <div class="changeset_file">
14 <div class="changeset_file">
15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
17 </div>
17 </div>
18 <span class="diff-actions">
18 <span class="diff-actions">
19 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" title="${_('diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
19 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" title="${_('diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
20 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw')}" title="${_('raw diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
20 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw')}" title="${_('raw diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download')}" title="${_('download diff')}"><img class="icon" src="${h.url('/images/icons/down_16.png')}"/></a>
21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download')}" title="${_('download diff')}"><img class="icon" src="${h.url('/images/icons/down_16.png')}"/></a>
22 ${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))}
22 ${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))}
23 ${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))}
23 ${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))}
24 </span>
24 </span>
25 <span style="float:right;margin-top:-3px">
25 <span style="float:right;margin-top:-3px">
26 <label>
26 <label>
27 ${_('show inline comments')}
27 ${_('show inline comments')}
28 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
28 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
29 </label>
29 </label>
30 </span>
30 </span>
31 </div>
31 </div>
32 </div>
32 </div>
33 <div class="code-body">
33 <div class="code-body">
34 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
34 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
35 ${diff|n}
35 ${diff|n}
36 </div>
36 </div>
37 </div>
37 </div>
38 %endif
38 %endif
39 %endfor
39 %endfor
40
40
41 </%def> No newline at end of file
41 </%def>
@@ -1,77 +1,79 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 %if c.repo_changesets:
2 %if c.repo_changesets:
3 <table class="table_disp">
3 <table class="table_disp">
4 <tr>
4 <tr>
5 <th class="left">${_('commit message')}</th>
5 <th class="left">${_('revision')}</th>
6 <th class="left">${_('commit message')}</th>
6 <th class="left">${_('age')}</th>
7 <th class="left">${_('age')}</th>
7 <th class="left">${_('author')}</th>
8 <th class="left">${_('author')}</th>
8 <th class="left">${_('revision')}</th>
9 <th class="left">${_('branch')}</th>
9 <th class="left">${_('branch')}</th>
10 <th class="left">${_('tags')}</th>
10 <th class="left">${_('tags')}</th>
11 </tr>
11 </tr>
12 %for cnt,cs in enumerate(c.repo_changesets):
12 %for cnt,cs in enumerate(c.repo_changesets):
13 <tr class="parity${cnt%2}">
13 <tr class="parity${cnt%2}">
14 <td>
14 <td>
15 <div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div>
16 </td>
17 <td>
15 ${h.link_to(h.truncate(cs.message,50),
18 ${h.link_to(h.truncate(cs.message,50),
16 h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
19 h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
17 title=cs.message)}
20 title=cs.message)}
18 </td>
21 </td>
19 <td><span class="tooltip" title="${cs.date}">
22 <td><span class="tooltip" title="${cs.date}">
20 ${h.age(cs.date)}</span>
23 ${h.age(cs.date)}</span>
21 </td>
24 </td>
22 <td title="${cs.author}">${h.person(cs.author)}</td>
25 <td title="${cs.author}">${h.person(cs.author)}</td>
23 <td><div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div></td>
24 <td>
26 <td>
25 <span class="logtags">
27 <span class="logtags">
26 <span class="branchtag">${cs.branch}</span>
28 <span class="branchtag">${cs.branch}</span>
27 </span>
29 </span>
28 </td>
30 </td>
29 <td>
31 <td>
30 <span class="logtags">
32 <span class="logtags">
31 %for tag in cs.tags:
33 %for tag in cs.tags:
32 <span class="tagtag">${tag}</span>
34 <span class="tagtag">${tag}</span>
33 %endfor
35 %endfor
34 </span>
36 </span>
35 </td>
37 </td>
36 </tr>
38 </tr>
37 %endfor
39 %endfor
38
40
39 </table>
41 </table>
40
42
41 <script type="text/javascript">
43 <script type="text/javascript">
42 YUE.onDOMReady(function(){
44 YUE.onDOMReady(function(){
43 YUE.delegate("shortlog_data","click",function(e, matchedEl, container){
45 YUE.delegate("shortlog_data","click",function(e, matchedEl, container){
44 ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();});
46 ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();});
45 YUE.preventDefault(e);
47 YUE.preventDefault(e);
46 },'.pager_link');
48 },'.pager_link');
47 });
49 });
48 </script>
50 </script>
49
51
50 <div class="pagination-wh pagination-left">
52 <div class="pagination-wh pagination-left">
51 ${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
53 ${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
52 </div>
54 </div>
53 %else:
55 %else:
54
56
55 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
57 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
56 <h4>${_('Add or upload files directly via RhodeCode')}</h4>
58 <h4>${_('Add or upload files directly via RhodeCode')}</h4>
57 <div style="margin: 20px 30px;">
59 <div style="margin: 20px 30px;">
58 <div id="add_node_id" class="add_node">
60 <div id="add_node_id" class="add_node">
59 <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='')}">${_('add new file')}</a>
61 <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='')}">${_('add new file')}</a>
60 </div>
62 </div>
61 </div>
63 </div>
62 %endif
64 %endif
63
65
64
66
65 <h4>${_('Push new repo')}</h4>
67 <h4>${_('Push new repo')}</h4>
66 <pre>
68 <pre>
67 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
69 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
68 ${c.rhodecode_repo.alias} add README # add first file
70 ${c.rhodecode_repo.alias} add README # add first file
69 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
71 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
70 ${c.rhodecode_repo.alias} push # push changes back
72 ${c.rhodecode_repo.alias} push # push changes back
71 </pre>
73 </pre>
72
74
73 <h4>${_('Existing repository?')}</h4>
75 <h4>${_('Existing repository?')}</h4>
74 <pre>
76 <pre>
75 ${c.rhodecode_repo.alias} push ${c.clone_repo_url}
77 ${c.rhodecode_repo.alias} push ${c.clone_repo_url}
76 </pre>
78 </pre>
77 %endif
79 %endif
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now