##// END OF EJS Templates
unicode: consistently use the preferred Python spelling 'utf-8' instead of the alias 'utf8'
Mads Kiilerich -
r7250:e2519d2e default
parent child Browse files
Show More
@@ -1,468 +1,468 b''
1 ################################################################################
1 ################################################################################
2 ################################################################################
2 ################################################################################
3 # Kallithea - config file generated with kallithea-config #
3 # Kallithea - config file generated with kallithea-config #
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
8
9 [DEFAULT]
9 [DEFAULT]
10
10
11 ################################################################################
11 ################################################################################
12 ## Email settings ##
12 ## Email settings ##
13 ## ##
13 ## ##
14 ## Refer to the documentation ("Email settings") for more details. ##
14 ## Refer to the documentation ("Email settings") for more details. ##
15 ## ##
15 ## ##
16 ## It is recommended to use a valid sender address that passes access ##
16 ## It is recommended to use a valid sender address that passes access ##
17 ## validation and spam filtering in mail servers. ##
17 ## validation and spam filtering in mail servers. ##
18 ################################################################################
18 ################################################################################
19
19
20 ## 'From' header for application emails. You can optionally add a name.
20 ## 'From' header for application emails. You can optionally add a name.
21 ## Default:
21 ## Default:
22 #app_email_from = Kallithea
22 #app_email_from = Kallithea
23 ## Examples:
23 ## Examples:
24 #app_email_from = Kallithea <kallithea-noreply@example.com>
24 #app_email_from = Kallithea <kallithea-noreply@example.com>
25 #app_email_from = kallithea-noreply@example.com
25 #app_email_from = kallithea-noreply@example.com
26
26
27 ## Subject prefix for application emails.
27 ## Subject prefix for application emails.
28 ## A space between this prefix and the real subject is automatically added.
28 ## A space between this prefix and the real subject is automatically added.
29 ## Default:
29 ## Default:
30 #email_prefix =
30 #email_prefix =
31 ## Example:
31 ## Example:
32 #email_prefix = [Kallithea]
32 #email_prefix = [Kallithea]
33
33
34 ## Recipients for error emails and fallback recipients of application mails.
34 ## Recipients for error emails and fallback recipients of application mails.
35 ## Multiple addresses can be specified, comma-separated.
35 ## Multiple addresses can be specified, comma-separated.
36 ## Only addresses are allowed, do not add any name part.
36 ## Only addresses are allowed, do not add any name part.
37 ## Default:
37 ## Default:
38 #email_to =
38 #email_to =
39 ## Examples:
39 ## Examples:
40 #email_to = admin@example.com
40 #email_to = admin@example.com
41 #email_to = admin@example.com,another_admin@example.com
41 #email_to = admin@example.com,another_admin@example.com
42 email_to =
42 email_to =
43
43
44 ## 'From' header for error emails. You can optionally add a name.
44 ## 'From' header for error emails. You can optionally add a name.
45 ## Default: (none)
45 ## Default: (none)
46 ## Examples:
46 ## Examples:
47 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
47 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
48 #error_email_from = kallithea_errors@example.com
48 #error_email_from = kallithea_errors@example.com
49 error_email_from =
49 error_email_from =
50
50
51 ## SMTP server settings
51 ## SMTP server settings
52 ## If specifying credentials, make sure to use secure connections.
52 ## If specifying credentials, make sure to use secure connections.
53 ## Default: Send unencrypted unauthenticated mails to the specified smtp_server.
53 ## Default: Send unencrypted unauthenticated mails to the specified smtp_server.
54 ## For "SSL", use smtp_use_ssl = true and smtp_port = 465.
54 ## For "SSL", use smtp_use_ssl = true and smtp_port = 465.
55 ## For "STARTTLS", use smtp_use_tls = true and smtp_port = 587.
55 ## For "STARTTLS", use smtp_use_tls = true and smtp_port = 587.
56 smtp_server =
56 smtp_server =
57 #smtp_username =
57 #smtp_username =
58 #smtp_password =
58 #smtp_password =
59 smtp_port =
59 smtp_port =
60 #smtp_use_ssl = false
60 #smtp_use_ssl = false
61 #smtp_use_tls = false
61 #smtp_use_tls = false
62
62
63 ## Entry point for 'gearbox serve'
63 ## Entry point for 'gearbox serve'
64 [server:main]
64 [server:main]
65 #host = 127.0.0.1
65 #host = 127.0.0.1
66 host = 0.0.0.0
66 host = 0.0.0.0
67 port = 5000
67 port = 5000
68
68
69 ## WAITRESS ##
69 ## WAITRESS ##
70 use = egg:waitress#main
70 use = egg:waitress#main
71 ## number of worker threads
71 ## number of worker threads
72 threads = 1
72 threads = 1
73 ## MAX BODY SIZE 100GB
73 ## MAX BODY SIZE 100GB
74 max_request_body_size = 107374182400
74 max_request_body_size = 107374182400
75 ## use poll instead of select, fixes fd limits, may not work on old
75 ## use poll instead of select, fixes fd limits, may not work on old
76 ## windows systems.
76 ## windows systems.
77 #asyncore_use_poll = True
77 #asyncore_use_poll = True
78
78
79 ## middleware for hosting the WSGI application under a URL prefix
79 ## middleware for hosting the WSGI application under a URL prefix
80 #[filter:proxy-prefix]
80 #[filter:proxy-prefix]
81 #use = egg:PasteDeploy#prefix
81 #use = egg:PasteDeploy#prefix
82 #prefix = /<your-prefix>
82 #prefix = /<your-prefix>
83
83
84 [app:main]
84 [app:main]
85 use = egg:kallithea
85 use = egg:kallithea
86 ## enable proxy prefix middleware
86 ## enable proxy prefix middleware
87 #filter-with = proxy-prefix
87 #filter-with = proxy-prefix
88
88
89 full_stack = true
89 full_stack = true
90 static_files = true
90 static_files = true
91
91
92 ## Internationalization (see setup documentation for details)
92 ## Internationalization (see setup documentation for details)
93 ## By default, the language requested by the browser is used if available.
93 ## By default, the language requested by the browser is used if available.
94 #i18n.enable = false
94 #i18n.enable = false
95 ## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):
95 ## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):
96 i18n.lang =
96 i18n.lang =
97
97
98 cache_dir = %(here)s/data
98 cache_dir = %(here)s/data
99 index_dir = %(here)s/data/index
99 index_dir = %(here)s/data/index
100
100
101 ## uncomment and set this path to use archive download cache
101 ## uncomment and set this path to use archive download cache
102 archive_cache_dir = %(here)s/tarballcache
102 archive_cache_dir = %(here)s/tarballcache
103
103
104 ## change this to unique ID for security
104 ## change this to unique ID for security
105 #app_instance_uuid = VERY-SECRET
105 #app_instance_uuid = VERY-SECRET
106 app_instance_uuid = development-not-secret
106 app_instance_uuid = development-not-secret
107
107
108 ## cut off limit for large diffs (size in bytes)
108 ## cut off limit for large diffs (size in bytes)
109 cut_off_limit = 256000
109 cut_off_limit = 256000
110
110
111 ## force https in Kallithea, fixes https redirects, assumes it's always https
111 ## force https in Kallithea, fixes https redirects, assumes it's always https
112 force_https = false
112 force_https = false
113
113
114 ## use Strict-Transport-Security headers
114 ## use Strict-Transport-Security headers
115 use_htsts = false
115 use_htsts = false
116
116
117 ## number of commits stats will parse on each iteration
117 ## number of commits stats will parse on each iteration
118 commit_parse_limit = 25
118 commit_parse_limit = 25
119
119
120 ## path to git executable
120 ## path to git executable
121 git_path = git
121 git_path = git
122
122
123 ## git rev filter option, --all is the default filter, if you need to
123 ## git rev filter option, --all is the default filter, if you need to
124 ## hide all refs in changelog switch this to --branches --tags
124 ## hide all refs in changelog switch this to --branches --tags
125 #git_rev_filter = --branches --tags
125 #git_rev_filter = --branches --tags
126
126
127 ## RSS feed options
127 ## RSS feed options
128 rss_cut_off_limit = 256000
128 rss_cut_off_limit = 256000
129 rss_items_per_page = 10
129 rss_items_per_page = 10
130 rss_include_diff = false
130 rss_include_diff = false
131
131
132 ## options for showing and identifying changesets
132 ## options for showing and identifying changesets
133 show_sha_length = 12
133 show_sha_length = 12
134 show_revision_number = false
134 show_revision_number = false
135
135
136 ## Canonical URL to use when creating full URLs in UI and texts.
136 ## Canonical URL to use when creating full URLs in UI and texts.
137 ## Useful when the site is available under different names or protocols.
137 ## Useful when the site is available under different names or protocols.
138 ## Defaults to what is provided in the WSGI environment.
138 ## Defaults to what is provided in the WSGI environment.
139 #canonical_url = https://kallithea.example.com/repos
139 #canonical_url = https://kallithea.example.com/repos
140
140
141 ## gist URL alias, used to create nicer urls for gist. This should be an
141 ## gist URL alias, used to create nicer urls for gist. This should be an
142 ## url that does rewrites to _admin/gists/<gistid>.
142 ## url that does rewrites to _admin/gists/<gistid>.
143 ## example: http://gist.example.com/{gistid}. Empty means use the internal
143 ## example: http://gist.example.com/{gistid}. Empty means use the internal
144 ## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid>
144 ## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid>
145 gist_alias_url =
145 gist_alias_url =
146
146
147 ## white list of API enabled controllers. This allows to add list of
147 ## white list of API enabled controllers. This allows to add list of
148 ## controllers to which access will be enabled by api_key. eg: to enable
148 ## controllers to which access will be enabled by api_key. eg: to enable
149 ## api access to raw_files put `FilesController:raw`, to enable access to patches
149 ## api access to raw_files put `FilesController:raw`, to enable access to patches
150 ## add `ChangesetController:changeset_patch`. This list should be "," separated
150 ## add `ChangesetController:changeset_patch`. This list should be "," separated
151 ## Syntax is <ControllerClass>:<function>. Check debug logs for generated names
151 ## Syntax is <ControllerClass>:<function>. Check debug logs for generated names
152 ## Recommended settings below are commented out:
152 ## Recommended settings below are commented out:
153 api_access_controllers_whitelist =
153 api_access_controllers_whitelist =
154 # ChangesetController:changeset_patch,
154 # ChangesetController:changeset_patch,
155 # ChangesetController:changeset_raw,
155 # ChangesetController:changeset_raw,
156 # FilesController:raw,
156 # FilesController:raw,
157 # FilesController:archivefile
157 # FilesController:archivefile
158
158
159 ## default encoding used to convert from and to unicode
159 ## default encoding used to convert from and to unicode
160 ## can be also a comma separated list of encoding in case of mixed encodings
160 ## can be also a comma separated list of encoding in case of mixed encodings
161 default_encoding = utf8
161 default_encoding = utf-8
162
162
163 ## Set Mercurial encoding, similar to setting HGENCODING before launching Kallithea
163 ## Set Mercurial encoding, similar to setting HGENCODING before launching Kallithea
164 hgencoding = utf-8
164 hgencoding = utf-8
165
165
166 ## issue tracker for Kallithea (leave blank to disable, absent for default)
166 ## issue tracker for Kallithea (leave blank to disable, absent for default)
167 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
167 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
168
168
169 ## issue tracking mapping for commit messages, comments, PR descriptions, ...
169 ## issue tracking mapping for commit messages, comments, PR descriptions, ...
170 ## Refer to the documentation ("Integration with issue trackers") for more details.
170 ## Refer to the documentation ("Integration with issue trackers") for more details.
171
171
172 ## regular expression to match issue references
172 ## regular expression to match issue references
173 ## This pattern may/should contain parenthesized groups, that can
173 ## This pattern may/should contain parenthesized groups, that can
174 ## be referred to in issue_server_link or issue_sub using Python backreferences
174 ## be referred to in issue_server_link or issue_sub using Python backreferences
175 ## (e.g. \1, \2, ...). You can also create named groups with '(?P<groupname>)'.
175 ## (e.g. \1, \2, ...). You can also create named groups with '(?P<groupname>)'.
176 ## To require mandatory whitespace before the issue pattern, use:
176 ## To require mandatory whitespace before the issue pattern, use:
177 ## (?:^|(?<=\s)) before the actual pattern, and for mandatory whitespace
177 ## (?:^|(?<=\s)) before the actual pattern, and for mandatory whitespace
178 ## behind the issue pattern, use (?:$|(?=\s)) after the actual pattern.
178 ## behind the issue pattern, use (?:$|(?=\s)) after the actual pattern.
179
179
180 issue_pat = #(\d+)
180 issue_pat = #(\d+)
181
181
182 ## server url to the issue
182 ## server url to the issue
183 ## This pattern may/should contain backreferences to parenthesized groups in issue_pat.
183 ## This pattern may/should contain backreferences to parenthesized groups in issue_pat.
184 ## A backreference can be \1, \2, ... or \g<groupname> if you specified a named group
184 ## A backreference can be \1, \2, ... or \g<groupname> if you specified a named group
185 ## called 'groupname' in issue_pat.
185 ## called 'groupname' in issue_pat.
186 ## The special token {repo} is replaced with the full repository name
186 ## The special token {repo} is replaced with the full repository name
187 ## including repository groups, while {repo_name} is replaced with just
187 ## including repository groups, while {repo_name} is replaced with just
188 ## the name of the repository.
188 ## the name of the repository.
189
189
190 issue_server_link = https://issues.example.com/{repo}/issue/\1
190 issue_server_link = https://issues.example.com/{repo}/issue/\1
191
191
192 ## substitution pattern to use as the link text
192 ## substitution pattern to use as the link text
193 ## If issue_sub is empty, the text matched by issue_pat is retained verbatim
193 ## If issue_sub is empty, the text matched by issue_pat is retained verbatim
194 ## for the link text. Otherwise, the link text is that of issue_sub, with any
194 ## for the link text. Otherwise, the link text is that of issue_sub, with any
195 ## backreferences to groups in issue_pat replaced.
195 ## backreferences to groups in issue_pat replaced.
196
196
197 issue_sub =
197 issue_sub =
198
198
199 ## issue_pat, issue_server_link and issue_sub can have suffixes to specify
199 ## issue_pat, issue_server_link and issue_sub can have suffixes to specify
200 ## multiple patterns, to other issues server, wiki or others
200 ## multiple patterns, to other issues server, wiki or others
201 ## below an example how to create a wiki pattern
201 ## below an example how to create a wiki pattern
202 # wiki-some-id -> https://wiki.example.com/some-id
202 # wiki-some-id -> https://wiki.example.com/some-id
203
203
204 #issue_pat_wiki = wiki-(\S+)
204 #issue_pat_wiki = wiki-(\S+)
205 #issue_server_link_wiki = https://wiki.example.com/\1
205 #issue_server_link_wiki = https://wiki.example.com/\1
206 #issue_sub_wiki = WIKI-\1
206 #issue_sub_wiki = WIKI-\1
207
207
208 ## alternative return HTTP header for failed authentication. Default HTTP
208 ## alternative return HTTP header for failed authentication. Default HTTP
209 ## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
209 ## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
210 ## handling that. Set this variable to 403 to return HTTPForbidden
210 ## handling that. Set this variable to 403 to return HTTPForbidden
211 auth_ret_code =
211 auth_ret_code =
212
212
213 ## locking return code. When repository is locked return this HTTP code. 2XX
213 ## locking return code. When repository is locked return this HTTP code. 2XX
214 ## codes don't break the transactions while 4XX codes do
214 ## codes don't break the transactions while 4XX codes do
215 lock_ret_code = 423
215 lock_ret_code = 423
216
216
217 ## allows to change the repository location in settings page
217 ## allows to change the repository location in settings page
218 allow_repo_location_change = True
218 allow_repo_location_change = True
219
219
220 ## allows to setup custom hooks in settings page
220 ## allows to setup custom hooks in settings page
221 allow_custom_hooks_settings = True
221 allow_custom_hooks_settings = True
222
222
223 ## extra extensions for indexing, space separated and without the leading '.'.
223 ## extra extensions for indexing, space separated and without the leading '.'.
224 # index.extensions =
224 # index.extensions =
225 # gemfile
225 # gemfile
226 # lock
226 # lock
227
227
228 ## extra filenames for indexing, space separated
228 ## extra filenames for indexing, space separated
229 # index.filenames =
229 # index.filenames =
230 # .dockerignore
230 # .dockerignore
231 # .editorconfig
231 # .editorconfig
232 # INSTALL
232 # INSTALL
233 # CHANGELOG
233 # CHANGELOG
234
234
235 ####################################
235 ####################################
236 ### CELERY CONFIG ####
236 ### CELERY CONFIG ####
237 ####################################
237 ####################################
238
238
239 use_celery = false
239 use_celery = false
240
240
241 ## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:
241 ## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:
242 broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
242 broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
243
243
244 celery.imports = kallithea.lib.celerylib.tasks
244 celery.imports = kallithea.lib.celerylib.tasks
245 celery.accept.content = pickle
245 celery.accept.content = pickle
246 celery.result.backend = amqp
246 celery.result.backend = amqp
247 celery.result.dburi = amqp://
247 celery.result.dburi = amqp://
248 celery.result.serialier = json
248 celery.result.serialier = json
249
249
250 #celery.send.task.error.emails = true
250 #celery.send.task.error.emails = true
251 #celery.amqp.task.result.expires = 18000
251 #celery.amqp.task.result.expires = 18000
252
252
253 celeryd.concurrency = 2
253 celeryd.concurrency = 2
254 celeryd.max.tasks.per.child = 1
254 celeryd.max.tasks.per.child = 1
255
255
256 ## If true, tasks will never be sent to the queue, but executed locally instead.
256 ## If true, tasks will never be sent to the queue, but executed locally instead.
257 celery.always.eager = false
257 celery.always.eager = false
258
258
259 ####################################
259 ####################################
260 ### BEAKER CACHE ####
260 ### BEAKER CACHE ####
261 ####################################
261 ####################################
262
262
263 beaker.cache.data_dir = %(here)s/data/cache/data
263 beaker.cache.data_dir = %(here)s/data/cache/data
264 beaker.cache.lock_dir = %(here)s/data/cache/lock
264 beaker.cache.lock_dir = %(here)s/data/cache/lock
265
265
266 beaker.cache.regions = short_term,long_term,sql_cache_short
266 beaker.cache.regions = short_term,long_term,sql_cache_short
267
267
268 beaker.cache.short_term.type = memory
268 beaker.cache.short_term.type = memory
269 beaker.cache.short_term.expire = 60
269 beaker.cache.short_term.expire = 60
270 beaker.cache.short_term.key_length = 256
270 beaker.cache.short_term.key_length = 256
271
271
272 beaker.cache.long_term.type = memory
272 beaker.cache.long_term.type = memory
273 beaker.cache.long_term.expire = 36000
273 beaker.cache.long_term.expire = 36000
274 beaker.cache.long_term.key_length = 256
274 beaker.cache.long_term.key_length = 256
275
275
276 beaker.cache.sql_cache_short.type = memory
276 beaker.cache.sql_cache_short.type = memory
277 beaker.cache.sql_cache_short.expire = 10
277 beaker.cache.sql_cache_short.expire = 10
278 beaker.cache.sql_cache_short.key_length = 256
278 beaker.cache.sql_cache_short.key_length = 256
279
279
280 ####################################
280 ####################################
281 ### BEAKER SESSION ####
281 ### BEAKER SESSION ####
282 ####################################
282 ####################################
283
283
284 ## Name of session cookie. Should be unique for a given host and path, even when running
284 ## Name of session cookie. Should be unique for a given host and path, even when running
285 ## on different ports. Otherwise, cookie sessions will be shared and messed up.
285 ## on different ports. Otherwise, cookie sessions will be shared and messed up.
286 beaker.session.key = kallithea
286 beaker.session.key = kallithea
287 ## Sessions should always only be accessible by the browser, not directly by JavaScript.
287 ## Sessions should always only be accessible by the browser, not directly by JavaScript.
288 beaker.session.httponly = true
288 beaker.session.httponly = true
289 ## Session lifetime. 2592000 seconds is 30 days.
289 ## Session lifetime. 2592000 seconds is 30 days.
290 beaker.session.timeout = 2592000
290 beaker.session.timeout = 2592000
291
291
292 ## Server secret used with HMAC to ensure integrity of cookies.
292 ## Server secret used with HMAC to ensure integrity of cookies.
293 #beaker.session.secret = VERY-SECRET
293 #beaker.session.secret = VERY-SECRET
294 beaker.session.secret = development-not-secret
294 beaker.session.secret = development-not-secret
295 ## Further, encrypt the data with AES.
295 ## Further, encrypt the data with AES.
296 #beaker.session.encrypt_key = <key_for_encryption>
296 #beaker.session.encrypt_key = <key_for_encryption>
297 #beaker.session.validate_key = <validation_key>
297 #beaker.session.validate_key = <validation_key>
298
298
299 ## Type of storage used for the session, current types are
299 ## Type of storage used for the session, current types are
300 ## dbm, file, memcached, database, and memory.
300 ## dbm, file, memcached, database, and memory.
301
301
302 ## File system storage of session data. (default)
302 ## File system storage of session data. (default)
303 #beaker.session.type = file
303 #beaker.session.type = file
304
304
305 ## Cookie only, store all session data inside the cookie. Requires secure secrets.
305 ## Cookie only, store all session data inside the cookie. Requires secure secrets.
306 #beaker.session.type = cookie
306 #beaker.session.type = cookie
307
307
308 ## Database storage of session data.
308 ## Database storage of session data.
309 #beaker.session.type = ext:database
309 #beaker.session.type = ext:database
310 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
310 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
311 #beaker.session.table_name = db_session
311 #beaker.session.table_name = db_session
312
312
313 ################################################################################
313 ################################################################################
314 ## WARNING: *DEBUG MODE MUST BE OFF IN A PRODUCTION ENVIRONMENT* ##
314 ## WARNING: *DEBUG MODE MUST BE OFF IN A PRODUCTION ENVIRONMENT* ##
315 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
315 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
316 ## execute malicious code after an exception is raised. ##
316 ## execute malicious code after an exception is raised. ##
317 ################################################################################
317 ################################################################################
318 #debug = false
318 #debug = false
319 debug = true
319 debug = true
320
320
321 ##################################
321 ##################################
322 ### LOGVIEW CONFIG ###
322 ### LOGVIEW CONFIG ###
323 ##################################
323 ##################################
324
324
325 logview.sqlalchemy = #faa
325 logview.sqlalchemy = #faa
326 logview.pylons.templating = #bfb
326 logview.pylons.templating = #bfb
327 logview.pylons.util = #eee
327 logview.pylons.util = #eee
328
328
329 #########################################################
329 #########################################################
330 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
330 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
331 #########################################################
331 #########################################################
332
332
333 # SQLITE [default]
333 # SQLITE [default]
334 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
334 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
335
335
336 # see sqlalchemy docs for others
336 # see sqlalchemy docs for others
337
337
338 sqlalchemy.pool_recycle = 3600
338 sqlalchemy.pool_recycle = 3600
339
339
340 ################################
340 ################################
341 ### ALEMBIC CONFIGURATION ####
341 ### ALEMBIC CONFIGURATION ####
342 ################################
342 ################################
343
343
344 [alembic]
344 [alembic]
345 script_location = kallithea:alembic
345 script_location = kallithea:alembic
346
346
347 ################################
347 ################################
348 ### LOGGING CONFIGURATION ####
348 ### LOGGING CONFIGURATION ####
349 ################################
349 ################################
350
350
351 [loggers]
351 [loggers]
352 keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer, werkzeug, backlash
352 keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer, werkzeug, backlash
353
353
354 [handlers]
354 [handlers]
355 keys = console, console_sql
355 keys = console, console_sql
356
356
357 [formatters]
357 [formatters]
358 keys = generic, color_formatter, color_formatter_sql
358 keys = generic, color_formatter, color_formatter_sql
359
359
360 #############
360 #############
361 ## LOGGERS ##
361 ## LOGGERS ##
362 #############
362 #############
363
363
364 [logger_root]
364 [logger_root]
365 level = NOTSET
365 level = NOTSET
366 handlers = console
366 handlers = console
367
367
368 [logger_routes]
368 [logger_routes]
369 #level = WARN
369 #level = WARN
370 level = DEBUG
370 level = DEBUG
371 handlers =
371 handlers =
372 qualname = routes.middleware
372 qualname = routes.middleware
373 ## "level = DEBUG" logs the route matched and routing variables.
373 ## "level = DEBUG" logs the route matched and routing variables.
374 propagate = 1
374 propagate = 1
375
375
376 [logger_beaker]
376 [logger_beaker]
377 #level = WARN
377 #level = WARN
378 level = DEBUG
378 level = DEBUG
379 handlers =
379 handlers =
380 qualname = beaker.container
380 qualname = beaker.container
381 propagate = 1
381 propagate = 1
382
382
383 [logger_templates]
383 [logger_templates]
384 #level = WARN
384 #level = WARN
385 level = INFO
385 level = INFO
386 handlers =
386 handlers =
387 qualname = pylons.templating
387 qualname = pylons.templating
388 propagate = 1
388 propagate = 1
389
389
390 [logger_kallithea]
390 [logger_kallithea]
391 #level = WARN
391 #level = WARN
392 level = DEBUG
392 level = DEBUG
393 handlers =
393 handlers =
394 qualname = kallithea
394 qualname = kallithea
395 propagate = 1
395 propagate = 1
396
396
397 [logger_tg]
397 [logger_tg]
398 #level = WARN
398 #level = WARN
399 level = DEBUG
399 level = DEBUG
400 handlers =
400 handlers =
401 qualname = tg
401 qualname = tg
402 propagate = 1
402 propagate = 1
403
403
404 [logger_gearbox]
404 [logger_gearbox]
405 #level = WARN
405 #level = WARN
406 level = DEBUG
406 level = DEBUG
407 handlers =
407 handlers =
408 qualname = gearbox
408 qualname = gearbox
409 propagate = 1
409 propagate = 1
410
410
411 [logger_sqlalchemy]
411 [logger_sqlalchemy]
412 level = WARN
412 level = WARN
413 handlers = console_sql
413 handlers = console_sql
414 qualname = sqlalchemy.engine
414 qualname = sqlalchemy.engine
415 propagate = 0
415 propagate = 0
416
416
417 [logger_whoosh_indexer]
417 [logger_whoosh_indexer]
418 #level = WARN
418 #level = WARN
419 level = DEBUG
419 level = DEBUG
420 handlers =
420 handlers =
421 qualname = whoosh_indexer
421 qualname = whoosh_indexer
422 propagate = 1
422 propagate = 1
423
423
424 [logger_werkzeug]
424 [logger_werkzeug]
425 level = WARN
425 level = WARN
426 handlers =
426 handlers =
427 qualname = werkzeug
427 qualname = werkzeug
428 propagate = 1
428 propagate = 1
429
429
430 [logger_backlash]
430 [logger_backlash]
431 level = WARN
431 level = WARN
432 handlers =
432 handlers =
433 qualname = backlash
433 qualname = backlash
434 propagate = 1
434 propagate = 1
435
435
436 ##############
436 ##############
437 ## HANDLERS ##
437 ## HANDLERS ##
438 ##############
438 ##############
439
439
440 [handler_console]
440 [handler_console]
441 class = StreamHandler
441 class = StreamHandler
442 args = (sys.stderr,)
442 args = (sys.stderr,)
443 #formatter = generic
443 #formatter = generic
444 formatter = color_formatter
444 formatter = color_formatter
445
445
446 [handler_console_sql]
446 [handler_console_sql]
447 class = StreamHandler
447 class = StreamHandler
448 args = (sys.stderr,)
448 args = (sys.stderr,)
449 #formatter = generic
449 #formatter = generic
450 formatter = color_formatter_sql
450 formatter = color_formatter_sql
451
451
452 ################
452 ################
453 ## FORMATTERS ##
453 ## FORMATTERS ##
454 ################
454 ################
455
455
456 [formatter_generic]
456 [formatter_generic]
457 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
457 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
458 datefmt = %Y-%m-%d %H:%M:%S
458 datefmt = %Y-%m-%d %H:%M:%S
459
459
460 [formatter_color_formatter]
460 [formatter_color_formatter]
461 class = kallithea.lib.colored_formatter.ColorFormatter
461 class = kallithea.lib.colored_formatter.ColorFormatter
462 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
462 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
463 datefmt = %Y-%m-%d %H:%M:%S
463 datefmt = %Y-%m-%d %H:%M:%S
464
464
465 [formatter_color_formatter_sql]
465 [formatter_color_formatter_sql]
466 class = kallithea.lib.colored_formatter.ColorFormatterSql
466 class = kallithea.lib.colored_formatter.ColorFormatterSql
467 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
467 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
468 datefmt = %Y-%m-%d %H:%M:%S
468 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,1283 +1,1283 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Helper functions
15 Helper functions
16
16
17 Consists of functions to typically be used within templates, but also
17 Consists of functions to typically be used within templates, but also
18 available to Controllers. This module is available to both as 'h'.
18 available to Controllers. This module is available to both as 'h'.
19 """
19 """
20 import hashlib
20 import hashlib
21 import json
21 import json
22 import StringIO
22 import StringIO
23 import logging
23 import logging
24 import re
24 import re
25 import urlparse
25 import urlparse
26 import textwrap
26 import textwrap
27
27
28 from beaker.cache import cache_region
28 from beaker.cache import cache_region
29 from pygments.formatters.html import HtmlFormatter
29 from pygments.formatters.html import HtmlFormatter
30 from pygments import highlight as code_highlight
30 from pygments import highlight as code_highlight
31 from tg.i18n import ugettext as _
31 from tg.i18n import ugettext as _
32
32
33 from webhelpers.html import literal, HTML, escape
33 from webhelpers.html import literal, HTML, escape
34 from webhelpers.html.tags import checkbox, end_form, hidden, link_to, \
34 from webhelpers.html.tags import checkbox, end_form, hidden, link_to, \
35 select, submit, text, password, textarea, radio, form as insecure_form
35 select, submit, text, password, textarea, radio, form as insecure_form
36 from webhelpers.number import format_byte_size
36 from webhelpers.number import format_byte_size
37 from webhelpers.pylonslib import Flash as _Flash
37 from webhelpers.pylonslib import Flash as _Flash
38 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
38 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
39 from webhelpers.text import chop_at, truncate, wrap_paragraphs
39 from webhelpers.text import chop_at, truncate, wrap_paragraphs
40 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
40 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
41 convert_boolean_attrs, NotGiven, _make_safe_id_component
41 convert_boolean_attrs, NotGiven, _make_safe_id_component
42
42
43 from kallithea.config.routing import url
43 from kallithea.config.routing import url
44 from kallithea.lib.annotate import annotate_highlight
44 from kallithea.lib.annotate import annotate_highlight
45 from kallithea.lib.pygmentsutils import get_custom_lexer
45 from kallithea.lib.pygmentsutils import get_custom_lexer
46 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
46 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
47 time_to_datetime, AttributeDict, safe_int, MENTIONS_REGEX
47 time_to_datetime, AttributeDict, safe_int, MENTIONS_REGEX
48 from kallithea.lib.markup_renderer import url_re
48 from kallithea.lib.markup_renderer import url_re
49 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
49 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
50 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
50 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 def canonical_url(*args, **kargs):
55 def canonical_url(*args, **kargs):
56 '''Like url(x, qualified=True), but returns url that not only is qualified
56 '''Like url(x, qualified=True), but returns url that not only is qualified
57 but also canonical, as configured in canonical_url'''
57 but also canonical, as configured in canonical_url'''
58 from kallithea import CONFIG
58 from kallithea import CONFIG
59 try:
59 try:
60 parts = CONFIG.get('canonical_url', '').split('://', 1)
60 parts = CONFIG.get('canonical_url', '').split('://', 1)
61 kargs['host'] = parts[1].split('/', 1)[0]
61 kargs['host'] = parts[1].split('/', 1)[0]
62 kargs['protocol'] = parts[0]
62 kargs['protocol'] = parts[0]
63 except IndexError:
63 except IndexError:
64 kargs['qualified'] = True
64 kargs['qualified'] = True
65 return url(*args, **kargs)
65 return url(*args, **kargs)
66
66
67
67
68 def canonical_hostname():
68 def canonical_hostname():
69 '''Return canonical hostname of system'''
69 '''Return canonical hostname of system'''
70 from kallithea import CONFIG
70 from kallithea import CONFIG
71 try:
71 try:
72 parts = CONFIG.get('canonical_url', '').split('://', 1)
72 parts = CONFIG.get('canonical_url', '').split('://', 1)
73 return parts[1].split('/', 1)[0]
73 return parts[1].split('/', 1)[0]
74 except IndexError:
74 except IndexError:
75 parts = url('home', qualified=True).split('://', 1)
75 parts = url('home', qualified=True).split('://', 1)
76 return parts[1].split('/', 1)[0]
76 return parts[1].split('/', 1)[0]
77
77
78
78
79 def html_escape(s):
79 def html_escape(s):
80 """Return string with all html escaped.
80 """Return string with all html escaped.
81 This is also safe for javascript in html but not necessarily correct.
81 This is also safe for javascript in html but not necessarily correct.
82 """
82 """
83 return (s
83 return (s
84 .replace('&', '&amp;')
84 .replace('&', '&amp;')
85 .replace(">", "&gt;")
85 .replace(">", "&gt;")
86 .replace("<", "&lt;")
86 .replace("<", "&lt;")
87 .replace('"', "&quot;")
87 .replace('"', "&quot;")
88 .replace("'", "&apos;") # Note: this is HTML5 not HTML4 and might not work in mails
88 .replace("'", "&apos;") # Note: this is HTML5 not HTML4 and might not work in mails
89 )
89 )
90
90
91 def js(value):
91 def js(value):
92 """Convert Python value to the corresponding JavaScript representation.
92 """Convert Python value to the corresponding JavaScript representation.
93
93
94 This is necessary to safely insert arbitrary values into HTML <script>
94 This is necessary to safely insert arbitrary values into HTML <script>
95 sections e.g. using Mako template expression substitution.
95 sections e.g. using Mako template expression substitution.
96
96
97 Note: Rather than using this function, it's preferable to avoid the
97 Note: Rather than using this function, it's preferable to avoid the
98 insertion of values into HTML <script> sections altogether. Instead,
98 insertion of values into HTML <script> sections altogether. Instead,
99 data should (to the extent possible) be passed to JavaScript using
99 data should (to the extent possible) be passed to JavaScript using
100 data attributes or AJAX calls, eliminating the need for JS specific
100 data attributes or AJAX calls, eliminating the need for JS specific
101 escaping.
101 escaping.
102
102
103 Note: This is not safe for use in attributes (e.g. onclick), because
103 Note: This is not safe for use in attributes (e.g. onclick), because
104 quotes are not escaped.
104 quotes are not escaped.
105
105
106 Because the rules for parsing <script> varies between XHTML (where
106 Because the rules for parsing <script> varies between XHTML (where
107 normal rules apply for any special characters) and HTML (where
107 normal rules apply for any special characters) and HTML (where
108 entities are not interpreted, but the literal string "</script>"
108 entities are not interpreted, but the literal string "</script>"
109 is forbidden), the function ensures that the result never contains
109 is forbidden), the function ensures that the result never contains
110 '&', '<' and '>', thus making it safe in both those contexts (but
110 '&', '<' and '>', thus making it safe in both those contexts (but
111 not in attributes).
111 not in attributes).
112 """
112 """
113 return literal(
113 return literal(
114 ('(' + json.dumps(value) + ')')
114 ('(' + json.dumps(value) + ')')
115 # In JSON, the following can only appear in string literals.
115 # In JSON, the following can only appear in string literals.
116 .replace('&', r'\x26')
116 .replace('&', r'\x26')
117 .replace('<', r'\x3c')
117 .replace('<', r'\x3c')
118 .replace('>', r'\x3e')
118 .replace('>', r'\x3e')
119 )
119 )
120
120
121
121
122 def jshtml(val):
122 def jshtml(val):
123 """HTML escapes a string value, then converts the resulting string
123 """HTML escapes a string value, then converts the resulting string
124 to its corresponding JavaScript representation (see `js`).
124 to its corresponding JavaScript representation (see `js`).
125
125
126 This is used when a plain-text string (possibly containing special
126 This is used when a plain-text string (possibly containing special
127 HTML characters) will be used by a script in an HTML context (e.g.
127 HTML characters) will be used by a script in an HTML context (e.g.
128 element.innerHTML or jQuery's 'html' method).
128 element.innerHTML or jQuery's 'html' method).
129
129
130 If in doubt, err on the side of using `jshtml` over `js`, since it's
130 If in doubt, err on the side of using `jshtml` over `js`, since it's
131 better to escape too much than too little.
131 better to escape too much than too little.
132 """
132 """
133 return js(escape(val))
133 return js(escape(val))
134
134
135
135
136 def shorter(s, size=20, firstline=False, postfix='...'):
136 def shorter(s, size=20, firstline=False, postfix='...'):
137 """Truncate s to size, including the postfix string if truncating.
137 """Truncate s to size, including the postfix string if truncating.
138 If firstline, truncate at newline.
138 If firstline, truncate at newline.
139 """
139 """
140 if firstline:
140 if firstline:
141 s = s.split('\n', 1)[0].rstrip()
141 s = s.split('\n', 1)[0].rstrip()
142 if len(s) > size:
142 if len(s) > size:
143 return s[:size - len(postfix)] + postfix
143 return s[:size - len(postfix)] + postfix
144 return s
144 return s
145
145
146
146
147 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
147 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
148 """
148 """
149 Reset button
149 Reset button
150 """
150 """
151 _set_input_attrs(attrs, type, name, value)
151 _set_input_attrs(attrs, type, name, value)
152 _set_id_attr(attrs, id, name)
152 _set_id_attr(attrs, id, name)
153 convert_boolean_attrs(attrs, ["disabled"])
153 convert_boolean_attrs(attrs, ["disabled"])
154 return HTML.input(**attrs)
154 return HTML.input(**attrs)
155
155
156
156
157 reset = _reset
157 reset = _reset
158 safeid = _make_safe_id_component
158 safeid = _make_safe_id_component
159
159
160
160
161 def FID(raw_id, path):
161 def FID(raw_id, path):
162 """
162 """
163 Creates a unique ID for filenode based on it's hash of path and revision
163 Creates a unique ID for filenode based on it's hash of path and revision
164 it's safe to use in urls
164 it's safe to use in urls
165
165
166 :param raw_id:
166 :param raw_id:
167 :param path:
167 :param path:
168 """
168 """
169
169
170 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
170 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
171
171
172
172
173 class _FilesBreadCrumbs(object):
173 class _FilesBreadCrumbs(object):
174
174
175 def __call__(self, repo_name, rev, paths):
175 def __call__(self, repo_name, rev, paths):
176 if isinstance(paths, str):
176 if isinstance(paths, str):
177 paths = safe_unicode(paths)
177 paths = safe_unicode(paths)
178 url_l = [link_to(repo_name, url('files_home',
178 url_l = [link_to(repo_name, url('files_home',
179 repo_name=repo_name,
179 repo_name=repo_name,
180 revision=rev, f_path=''),
180 revision=rev, f_path=''),
181 class_='ypjax-link')]
181 class_='ypjax-link')]
182 paths_l = paths.split('/')
182 paths_l = paths.split('/')
183 for cnt, p in enumerate(paths_l):
183 for cnt, p in enumerate(paths_l):
184 if p != '':
184 if p != '':
185 url_l.append(link_to(p,
185 url_l.append(link_to(p,
186 url('files_home',
186 url('files_home',
187 repo_name=repo_name,
187 repo_name=repo_name,
188 revision=rev,
188 revision=rev,
189 f_path='/'.join(paths_l[:cnt + 1])
189 f_path='/'.join(paths_l[:cnt + 1])
190 ),
190 ),
191 class_='ypjax-link'
191 class_='ypjax-link'
192 )
192 )
193 )
193 )
194
194
195 return literal('/'.join(url_l))
195 return literal('/'.join(url_l))
196
196
197
197
198 files_breadcrumbs = _FilesBreadCrumbs()
198 files_breadcrumbs = _FilesBreadCrumbs()
199
199
200
200
201 class CodeHtmlFormatter(HtmlFormatter):
201 class CodeHtmlFormatter(HtmlFormatter):
202 """
202 """
203 My code Html Formatter for source codes
203 My code Html Formatter for source codes
204 """
204 """
205
205
206 def wrap(self, source, outfile):
206 def wrap(self, source, outfile):
207 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
207 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
208
208
209 def _wrap_code(self, source):
209 def _wrap_code(self, source):
210 for cnt, it in enumerate(source):
210 for cnt, it in enumerate(source):
211 i, t = it
211 i, t = it
212 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
212 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
213 yield i, t
213 yield i, t
214
214
215 def _wrap_tablelinenos(self, inner):
215 def _wrap_tablelinenos(self, inner):
216 dummyoutfile = StringIO.StringIO()
216 dummyoutfile = StringIO.StringIO()
217 lncount = 0
217 lncount = 0
218 for t, line in inner:
218 for t, line in inner:
219 if t:
219 if t:
220 lncount += 1
220 lncount += 1
221 dummyoutfile.write(line)
221 dummyoutfile.write(line)
222
222
223 fl = self.linenostart
223 fl = self.linenostart
224 mw = len(str(lncount + fl - 1))
224 mw = len(str(lncount + fl - 1))
225 sp = self.linenospecial
225 sp = self.linenospecial
226 st = self.linenostep
226 st = self.linenostep
227 la = self.lineanchors
227 la = self.lineanchors
228 aln = self.anchorlinenos
228 aln = self.anchorlinenos
229 nocls = self.noclasses
229 nocls = self.noclasses
230 if sp:
230 if sp:
231 lines = []
231 lines = []
232
232
233 for i in range(fl, fl + lncount):
233 for i in range(fl, fl + lncount):
234 if i % st == 0:
234 if i % st == 0:
235 if i % sp == 0:
235 if i % sp == 0:
236 if aln:
236 if aln:
237 lines.append('<a href="#%s%d" class="special">%*d</a>' %
237 lines.append('<a href="#%s%d" class="special">%*d</a>' %
238 (la, i, mw, i))
238 (la, i, mw, i))
239 else:
239 else:
240 lines.append('<span class="special">%*d</span>' % (mw, i))
240 lines.append('<span class="special">%*d</span>' % (mw, i))
241 else:
241 else:
242 if aln:
242 if aln:
243 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
243 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
244 else:
244 else:
245 lines.append('%*d' % (mw, i))
245 lines.append('%*d' % (mw, i))
246 else:
246 else:
247 lines.append('')
247 lines.append('')
248 ls = '\n'.join(lines)
248 ls = '\n'.join(lines)
249 else:
249 else:
250 lines = []
250 lines = []
251 for i in range(fl, fl + lncount):
251 for i in range(fl, fl + lncount):
252 if i % st == 0:
252 if i % st == 0:
253 if aln:
253 if aln:
254 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
254 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
255 else:
255 else:
256 lines.append('%*d' % (mw, i))
256 lines.append('%*d' % (mw, i))
257 else:
257 else:
258 lines.append('')
258 lines.append('')
259 ls = '\n'.join(lines)
259 ls = '\n'.join(lines)
260
260
261 # in case you wonder about the seemingly redundant <div> here: since the
261 # in case you wonder about the seemingly redundant <div> here: since the
262 # content in the other cell also is wrapped in a div, some browsers in
262 # content in the other cell also is wrapped in a div, some browsers in
263 # some configurations seem to mess up the formatting...
263 # some configurations seem to mess up the formatting...
264 if nocls:
264 if nocls:
265 yield 0, ('<table class="%stable">' % self.cssclass +
265 yield 0, ('<table class="%stable">' % self.cssclass +
266 '<tr><td><div class="linenodiv">'
266 '<tr><td><div class="linenodiv">'
267 '<pre>' + ls + '</pre></div></td>'
267 '<pre>' + ls + '</pre></div></td>'
268 '<td id="hlcode" class="code">')
268 '<td id="hlcode" class="code">')
269 else:
269 else:
270 yield 0, ('<table class="%stable">' % self.cssclass +
270 yield 0, ('<table class="%stable">' % self.cssclass +
271 '<tr><td class="linenos"><div class="linenodiv">'
271 '<tr><td class="linenos"><div class="linenodiv">'
272 '<pre>' + ls + '</pre></div></td>'
272 '<pre>' + ls + '</pre></div></td>'
273 '<td id="hlcode" class="code">')
273 '<td id="hlcode" class="code">')
274 yield 0, dummyoutfile.getvalue()
274 yield 0, dummyoutfile.getvalue()
275 yield 0, '</td></tr></table>'
275 yield 0, '</td></tr></table>'
276
276
277
277
278 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
278 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
279
279
280
280
281 def _markup_whitespace(m):
281 def _markup_whitespace(m):
282 groups = m.groups()
282 groups = m.groups()
283 if groups[0]:
283 if groups[0]:
284 return '<u>\t</u>'
284 return '<u>\t</u>'
285 if groups[1]:
285 if groups[1]:
286 return ' <i></i>'
286 return ' <i></i>'
287
287
288
288
289 def markup_whitespace(s):
289 def markup_whitespace(s):
290 return _whitespace_re.sub(_markup_whitespace, s)
290 return _whitespace_re.sub(_markup_whitespace, s)
291
291
292
292
293 def pygmentize(filenode, **kwargs):
293 def pygmentize(filenode, **kwargs):
294 """
294 """
295 pygmentize function using pygments
295 pygmentize function using pygments
296
296
297 :param filenode:
297 :param filenode:
298 """
298 """
299 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
299 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
300 return literal(markup_whitespace(
300 return literal(markup_whitespace(
301 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
301 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
302
302
303
303
304 def pygmentize_annotation(repo_name, filenode, **kwargs):
304 def pygmentize_annotation(repo_name, filenode, **kwargs):
305 """
305 """
306 pygmentize function for annotation
306 pygmentize function for annotation
307
307
308 :param filenode:
308 :param filenode:
309 """
309 """
310
310
311 color_dict = {}
311 color_dict = {}
312
312
313 def gen_color(n=10000):
313 def gen_color(n=10000):
314 """generator for getting n of evenly distributed colors using
314 """generator for getting n of evenly distributed colors using
315 hsv color and golden ratio. It always return same order of colors
315 hsv color and golden ratio. It always return same order of colors
316
316
317 :returns: RGB tuple
317 :returns: RGB tuple
318 """
318 """
319
319
320 def hsv_to_rgb(h, s, v):
320 def hsv_to_rgb(h, s, v):
321 if s == 0.0:
321 if s == 0.0:
322 return v, v, v
322 return v, v, v
323 i = int(h * 6.0) # XXX assume int() truncates!
323 i = int(h * 6.0) # XXX assume int() truncates!
324 f = (h * 6.0) - i
324 f = (h * 6.0) - i
325 p = v * (1.0 - s)
325 p = v * (1.0 - s)
326 q = v * (1.0 - s * f)
326 q = v * (1.0 - s * f)
327 t = v * (1.0 - s * (1.0 - f))
327 t = v * (1.0 - s * (1.0 - f))
328 i = i % 6
328 i = i % 6
329 if i == 0:
329 if i == 0:
330 return v, t, p
330 return v, t, p
331 if i == 1:
331 if i == 1:
332 return q, v, p
332 return q, v, p
333 if i == 2:
333 if i == 2:
334 return p, v, t
334 return p, v, t
335 if i == 3:
335 if i == 3:
336 return p, q, v
336 return p, q, v
337 if i == 4:
337 if i == 4:
338 return t, p, v
338 return t, p, v
339 if i == 5:
339 if i == 5:
340 return v, p, q
340 return v, p, q
341
341
342 golden_ratio = 0.618033988749895
342 golden_ratio = 0.618033988749895
343 h = 0.22717784590367374
343 h = 0.22717784590367374
344
344
345 for _unused in xrange(n):
345 for _unused in xrange(n):
346 h += golden_ratio
346 h += golden_ratio
347 h %= 1
347 h %= 1
348 HSV_tuple = [h, 0.95, 0.95]
348 HSV_tuple = [h, 0.95, 0.95]
349 RGB_tuple = hsv_to_rgb(*HSV_tuple)
349 RGB_tuple = hsv_to_rgb(*HSV_tuple)
350 yield map(lambda x: str(int(x * 256)), RGB_tuple)
350 yield map(lambda x: str(int(x * 256)), RGB_tuple)
351
351
352 cgenerator = gen_color()
352 cgenerator = gen_color()
353
353
354 def get_color_string(cs):
354 def get_color_string(cs):
355 if cs in color_dict:
355 if cs in color_dict:
356 col = color_dict[cs]
356 col = color_dict[cs]
357 else:
357 else:
358 col = color_dict[cs] = cgenerator.next()
358 col = color_dict[cs] = cgenerator.next()
359 return "color: rgb(%s)! important;" % (', '.join(col))
359 return "color: rgb(%s)! important;" % (', '.join(col))
360
360
361 def url_func(repo_name):
361 def url_func(repo_name):
362
362
363 def _url_func(changeset):
363 def _url_func(changeset):
364 author = escape(changeset.author)
364 author = escape(changeset.author)
365 date = changeset.date
365 date = changeset.date
366 message = escape(changeset.message)
366 message = escape(changeset.message)
367 tooltip_html = ("<b>Author:</b> %s<br/>"
367 tooltip_html = ("<b>Author:</b> %s<br/>"
368 "<b>Date:</b> %s</b><br/>"
368 "<b>Date:</b> %s</b><br/>"
369 "<b>Message:</b> %s") % (author, date, message)
369 "<b>Message:</b> %s") % (author, date, message)
370
370
371 lnk_format = show_id(changeset)
371 lnk_format = show_id(changeset)
372 uri = link_to(
372 uri = link_to(
373 lnk_format,
373 lnk_format,
374 url('changeset_home', repo_name=repo_name,
374 url('changeset_home', repo_name=repo_name,
375 revision=changeset.raw_id),
375 revision=changeset.raw_id),
376 style=get_color_string(changeset.raw_id),
376 style=get_color_string(changeset.raw_id),
377 **{'data-toggle': 'popover',
377 **{'data-toggle': 'popover',
378 'data-content': tooltip_html}
378 'data-content': tooltip_html}
379 )
379 )
380
380
381 uri += '\n'
381 uri += '\n'
382 return uri
382 return uri
383 return _url_func
383 return _url_func
384
384
385 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
385 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
386
386
387
387
388 class _Message(object):
388 class _Message(object):
389 """A message returned by ``Flash.pop_messages()``.
389 """A message returned by ``Flash.pop_messages()``.
390
390
391 Converting the message to a string returns the message text. Instances
391 Converting the message to a string returns the message text. Instances
392 also have the following attributes:
392 also have the following attributes:
393
393
394 * ``message``: the message text.
394 * ``message``: the message text.
395 * ``category``: the category specified when the message was created.
395 * ``category``: the category specified when the message was created.
396 """
396 """
397
397
398 def __init__(self, category, message):
398 def __init__(self, category, message):
399 self.category = category
399 self.category = category
400 self.message = message
400 self.message = message
401
401
402 def __str__(self):
402 def __str__(self):
403 return self.message
403 return self.message
404
404
405 __unicode__ = __str__
405 __unicode__ = __str__
406
406
407 def __html__(self):
407 def __html__(self):
408 return escape(safe_unicode(self.message))
408 return escape(safe_unicode(self.message))
409
409
410
410
411 class Flash(_Flash):
411 class Flash(_Flash):
412
412
413 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
413 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
414 """
414 """
415 Show a message to the user _and_ log it through the specified function
415 Show a message to the user _and_ log it through the specified function
416
416
417 category: notice (default), warning, error, success
417 category: notice (default), warning, error, success
418 logf: a custom log function - such as log.debug
418 logf: a custom log function - such as log.debug
419
419
420 logf defaults to log.info, unless category equals 'success', in which
420 logf defaults to log.info, unless category equals 'success', in which
421 case logf defaults to log.debug.
421 case logf defaults to log.debug.
422 """
422 """
423 if logf is None:
423 if logf is None:
424 logf = log.info
424 logf = log.info
425 if category == 'success':
425 if category == 'success':
426 logf = log.debug
426 logf = log.debug
427
427
428 logf('Flash %s: %s', category, message)
428 logf('Flash %s: %s', category, message)
429
429
430 super(Flash, self).__call__(message, category, ignore_duplicate)
430 super(Flash, self).__call__(message, category, ignore_duplicate)
431
431
432 def pop_messages(self):
432 def pop_messages(self):
433 """Return all accumulated messages and delete them from the session.
433 """Return all accumulated messages and delete them from the session.
434
434
435 The return value is a list of ``Message`` objects.
435 The return value is a list of ``Message`` objects.
436 """
436 """
437 from tg import session
437 from tg import session
438 messages = session.pop(self.session_key, [])
438 messages = session.pop(self.session_key, [])
439 session.save()
439 session.save()
440 return [_Message(*m) for m in messages]
440 return [_Message(*m) for m in messages]
441
441
442
442
443 flash = Flash()
443 flash = Flash()
444
444
445 #==============================================================================
445 #==============================================================================
446 # SCM FILTERS available via h.
446 # SCM FILTERS available via h.
447 #==============================================================================
447 #==============================================================================
448 from kallithea.lib.vcs.utils import author_name, author_email
448 from kallithea.lib.vcs.utils import author_name, author_email
449 from kallithea.lib.utils2 import credentials_filter, age as _age
449 from kallithea.lib.utils2 import credentials_filter, age as _age
450
450
451 age = lambda x, y=False: _age(x, y)
451 age = lambda x, y=False: _age(x, y)
452 capitalize = lambda x: x.capitalize()
452 capitalize = lambda x: x.capitalize()
453 email = author_email
453 email = author_email
454 short_id = lambda x: x[:12]
454 short_id = lambda x: x[:12]
455 hide_credentials = lambda x: ''.join(credentials_filter(x))
455 hide_credentials = lambda x: ''.join(credentials_filter(x))
456
456
457
457
458 def show_id(cs):
458 def show_id(cs):
459 """
459 """
460 Configurable function that shows ID
460 Configurable function that shows ID
461 by default it's r123:fffeeefffeee
461 by default it's r123:fffeeefffeee
462
462
463 :param cs: changeset instance
463 :param cs: changeset instance
464 """
464 """
465 from kallithea import CONFIG
465 from kallithea import CONFIG
466 def_len = safe_int(CONFIG.get('show_sha_length', 12))
466 def_len = safe_int(CONFIG.get('show_sha_length', 12))
467 show_rev = str2bool(CONFIG.get('show_revision_number', False))
467 show_rev = str2bool(CONFIG.get('show_revision_number', False))
468
468
469 raw_id = cs.raw_id[:def_len]
469 raw_id = cs.raw_id[:def_len]
470 if show_rev:
470 if show_rev:
471 return 'r%s:%s' % (cs.revision, raw_id)
471 return 'r%s:%s' % (cs.revision, raw_id)
472 else:
472 else:
473 return raw_id
473 return raw_id
474
474
475
475
476 def fmt_date(date):
476 def fmt_date(date):
477 if date:
477 if date:
478 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
478 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf-8')
479
479
480 return ""
480 return ""
481
481
482
482
483 def is_git(repository):
483 def is_git(repository):
484 if hasattr(repository, 'alias'):
484 if hasattr(repository, 'alias'):
485 _type = repository.alias
485 _type = repository.alias
486 elif hasattr(repository, 'repo_type'):
486 elif hasattr(repository, 'repo_type'):
487 _type = repository.repo_type
487 _type = repository.repo_type
488 else:
488 else:
489 _type = repository
489 _type = repository
490 return _type == 'git'
490 return _type == 'git'
491
491
492
492
493 def is_hg(repository):
493 def is_hg(repository):
494 if hasattr(repository, 'alias'):
494 if hasattr(repository, 'alias'):
495 _type = repository.alias
495 _type = repository.alias
496 elif hasattr(repository, 'repo_type'):
496 elif hasattr(repository, 'repo_type'):
497 _type = repository.repo_type
497 _type = repository.repo_type
498 else:
498 else:
499 _type = repository
499 _type = repository
500 return _type == 'hg'
500 return _type == 'hg'
501
501
502
502
503 @cache_region('long_term', 'user_or_none')
503 @cache_region('long_term', 'user_or_none')
504 def user_or_none(author):
504 def user_or_none(author):
505 """Try to match email part of VCS committer string with a local user - or return None"""
505 """Try to match email part of VCS committer string with a local user - or return None"""
506 from kallithea.model.db import User
506 from kallithea.model.db import User
507 email = author_email(author)
507 email = author_email(author)
508 if email:
508 if email:
509 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
509 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
510 return None
510 return None
511
511
512
512
513 def email_or_none(author):
513 def email_or_none(author):
514 """Try to match email part of VCS committer string with a local user.
514 """Try to match email part of VCS committer string with a local user.
515 Return primary email of user, email part of the specified author name, or None."""
515 Return primary email of user, email part of the specified author name, or None."""
516 if not author:
516 if not author:
517 return None
517 return None
518 user = user_or_none(author)
518 user = user_or_none(author)
519 if user is not None:
519 if user is not None:
520 return user.email # always use main email address - not necessarily the one used to find user
520 return user.email # always use main email address - not necessarily the one used to find user
521
521
522 # extract email from the commit string
522 # extract email from the commit string
523 email = author_email(author)
523 email = author_email(author)
524 if email:
524 if email:
525 return email
525 return email
526
526
527 # No valid email, not a valid user in the system, none!
527 # No valid email, not a valid user in the system, none!
528 return None
528 return None
529
529
530
530
531 def person(author, show_attr="username"):
531 def person(author, show_attr="username"):
532 """Find the user identified by 'author', return one of the users attributes,
532 """Find the user identified by 'author', return one of the users attributes,
533 default to the username attribute, None if there is no user"""
533 default to the username attribute, None if there is no user"""
534 from kallithea.model.db import User
534 from kallithea.model.db import User
535 # attr to return from fetched user
535 # attr to return from fetched user
536 person_getter = lambda usr: getattr(usr, show_attr)
536 person_getter = lambda usr: getattr(usr, show_attr)
537
537
538 # if author is already an instance use it for extraction
538 # if author is already an instance use it for extraction
539 if isinstance(author, User):
539 if isinstance(author, User):
540 return person_getter(author)
540 return person_getter(author)
541
541
542 user = user_or_none(author)
542 user = user_or_none(author)
543 if user is not None:
543 if user is not None:
544 return person_getter(user)
544 return person_getter(user)
545
545
546 # Still nothing? Just pass back the author name if any, else the email
546 # Still nothing? Just pass back the author name if any, else the email
547 return author_name(author) or email(author)
547 return author_name(author) or email(author)
548
548
549
549
550 def person_by_id(id_, show_attr="username"):
550 def person_by_id(id_, show_attr="username"):
551 from kallithea.model.db import User
551 from kallithea.model.db import User
552 # attr to return from fetched user
552 # attr to return from fetched user
553 person_getter = lambda usr: getattr(usr, show_attr)
553 person_getter = lambda usr: getattr(usr, show_attr)
554
554
555 # maybe it's an ID ?
555 # maybe it's an ID ?
556 if str(id_).isdigit() or isinstance(id_, int):
556 if str(id_).isdigit() or isinstance(id_, int):
557 id_ = int(id_)
557 id_ = int(id_)
558 user = User.get(id_)
558 user = User.get(id_)
559 if user is not None:
559 if user is not None:
560 return person_getter(user)
560 return person_getter(user)
561 return id_
561 return id_
562
562
563
563
564 def boolicon(value):
564 def boolicon(value):
565 """Returns boolean value of a value, represented as small html image of true/false
565 """Returns boolean value of a value, represented as small html image of true/false
566 icons
566 icons
567
567
568 :param value: value
568 :param value: value
569 """
569 """
570
570
571 if value:
571 if value:
572 return HTML.tag('i', class_="icon-ok")
572 return HTML.tag('i', class_="icon-ok")
573 else:
573 else:
574 return HTML.tag('i', class_="icon-minus-circled")
574 return HTML.tag('i', class_="icon-minus-circled")
575
575
576
576
577 def action_parser(user_log, feed=False, parse_cs=False):
577 def action_parser(user_log, feed=False, parse_cs=False):
578 """
578 """
579 This helper will action_map the specified string action into translated
579 This helper will action_map the specified string action into translated
580 fancy names with icons and links
580 fancy names with icons and links
581
581
582 :param user_log: user log instance
582 :param user_log: user log instance
583 :param feed: use output for feeds (no html and fancy icons)
583 :param feed: use output for feeds (no html and fancy icons)
584 :param parse_cs: parse Changesets into VCS instances
584 :param parse_cs: parse Changesets into VCS instances
585 """
585 """
586
586
587 action = user_log.action
587 action = user_log.action
588 action_params = ' '
588 action_params = ' '
589
589
590 x = action.split(':')
590 x = action.split(':')
591
591
592 if len(x) > 1:
592 if len(x) > 1:
593 action, action_params = x
593 action, action_params = x
594
594
595 def get_cs_links():
595 def get_cs_links():
596 revs_limit = 3 # display this amount always
596 revs_limit = 3 # display this amount always
597 revs_top_limit = 50 # show upto this amount of changesets hidden
597 revs_top_limit = 50 # show upto this amount of changesets hidden
598 revs_ids = action_params.split(',')
598 revs_ids = action_params.split(',')
599 deleted = user_log.repository is None
599 deleted = user_log.repository is None
600 if deleted:
600 if deleted:
601 return ','.join(revs_ids)
601 return ','.join(revs_ids)
602
602
603 repo_name = user_log.repository.repo_name
603 repo_name = user_log.repository.repo_name
604
604
605 def lnk(rev, repo_name):
605 def lnk(rev, repo_name):
606 lazy_cs = False
606 lazy_cs = False
607 title_ = None
607 title_ = None
608 url_ = '#'
608 url_ = '#'
609 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
609 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
610 if rev.op and rev.ref_name:
610 if rev.op and rev.ref_name:
611 if rev.op == 'delete_branch':
611 if rev.op == 'delete_branch':
612 lbl = _('Deleted branch: %s') % rev.ref_name
612 lbl = _('Deleted branch: %s') % rev.ref_name
613 elif rev.op == 'tag':
613 elif rev.op == 'tag':
614 lbl = _('Created tag: %s') % rev.ref_name
614 lbl = _('Created tag: %s') % rev.ref_name
615 else:
615 else:
616 lbl = 'Unknown operation %s' % rev.op
616 lbl = 'Unknown operation %s' % rev.op
617 else:
617 else:
618 lazy_cs = True
618 lazy_cs = True
619 lbl = rev.short_id[:8]
619 lbl = rev.short_id[:8]
620 url_ = url('changeset_home', repo_name=repo_name,
620 url_ = url('changeset_home', repo_name=repo_name,
621 revision=rev.raw_id)
621 revision=rev.raw_id)
622 else:
622 else:
623 # changeset cannot be found - it might have been stripped or removed
623 # changeset cannot be found - it might have been stripped or removed
624 lbl = rev[:12]
624 lbl = rev[:12]
625 title_ = _('Changeset %s not found') % lbl
625 title_ = _('Changeset %s not found') % lbl
626 if parse_cs:
626 if parse_cs:
627 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
627 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
628 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
628 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
629 **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
629 **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
630
630
631 def _get_op(rev_txt):
631 def _get_op(rev_txt):
632 _op = None
632 _op = None
633 _name = rev_txt
633 _name = rev_txt
634 if len(rev_txt.split('=>')) == 2:
634 if len(rev_txt.split('=>')) == 2:
635 _op, _name = rev_txt.split('=>')
635 _op, _name = rev_txt.split('=>')
636 return _op, _name
636 return _op, _name
637
637
638 revs = []
638 revs = []
639 if len(filter(lambda v: v != '', revs_ids)) > 0:
639 if len(filter(lambda v: v != '', revs_ids)) > 0:
640 repo = None
640 repo = None
641 for rev in revs_ids[:revs_top_limit]:
641 for rev in revs_ids[:revs_top_limit]:
642 _op, _name = _get_op(rev)
642 _op, _name = _get_op(rev)
643
643
644 # we want parsed changesets, or new log store format is bad
644 # we want parsed changesets, or new log store format is bad
645 if parse_cs:
645 if parse_cs:
646 try:
646 try:
647 if repo is None:
647 if repo is None:
648 repo = user_log.repository.scm_instance
648 repo = user_log.repository.scm_instance
649 _rev = repo.get_changeset(rev)
649 _rev = repo.get_changeset(rev)
650 revs.append(_rev)
650 revs.append(_rev)
651 except ChangesetDoesNotExistError:
651 except ChangesetDoesNotExistError:
652 log.error('cannot find revision %s in this repo', rev)
652 log.error('cannot find revision %s in this repo', rev)
653 revs.append(rev)
653 revs.append(rev)
654 else:
654 else:
655 _rev = AttributeDict({
655 _rev = AttributeDict({
656 'short_id': rev[:12],
656 'short_id': rev[:12],
657 'raw_id': rev,
657 'raw_id': rev,
658 'message': '',
658 'message': '',
659 'op': _op,
659 'op': _op,
660 'ref_name': _name
660 'ref_name': _name
661 })
661 })
662 revs.append(_rev)
662 revs.append(_rev)
663 cs_links = [" " + ', '.join(
663 cs_links = [" " + ', '.join(
664 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
664 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
665 )]
665 )]
666 _op1, _name1 = _get_op(revs_ids[0])
666 _op1, _name1 = _get_op(revs_ids[0])
667 _op2, _name2 = _get_op(revs_ids[-1])
667 _op2, _name2 = _get_op(revs_ids[-1])
668
668
669 _rev = '%s...%s' % (_name1, _name2)
669 _rev = '%s...%s' % (_name1, _name2)
670
670
671 compare_view = (
671 compare_view = (
672 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
672 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
673 '<a href="%s">%s</a> </div>' % (
673 '<a href="%s">%s</a> </div>' % (
674 _('Show all combined changesets %s->%s') % (
674 _('Show all combined changesets %s->%s') % (
675 revs_ids[0][:12], revs_ids[-1][:12]
675 revs_ids[0][:12], revs_ids[-1][:12]
676 ),
676 ),
677 url('changeset_home', repo_name=repo_name,
677 url('changeset_home', repo_name=repo_name,
678 revision=_rev
678 revision=_rev
679 ),
679 ),
680 _('Compare view')
680 _('Compare view')
681 )
681 )
682 )
682 )
683
683
684 # if we have exactly one more than normally displayed
684 # if we have exactly one more than normally displayed
685 # just display it, takes less space than displaying
685 # just display it, takes less space than displaying
686 # "and 1 more revisions"
686 # "and 1 more revisions"
687 if len(revs_ids) == revs_limit + 1:
687 if len(revs_ids) == revs_limit + 1:
688 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
688 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
689
689
690 # hidden-by-default ones
690 # hidden-by-default ones
691 if len(revs_ids) > revs_limit + 1:
691 if len(revs_ids) > revs_limit + 1:
692 uniq_id = revs_ids[0]
692 uniq_id = revs_ids[0]
693 html_tmpl = (
693 html_tmpl = (
694 '<span> %s <a class="show_more" id="_%s" '
694 '<span> %s <a class="show_more" id="_%s" '
695 'href="#more">%s</a> %s</span>'
695 'href="#more">%s</a> %s</span>'
696 )
696 )
697 if not feed:
697 if not feed:
698 cs_links.append(html_tmpl % (
698 cs_links.append(html_tmpl % (
699 _('and'),
699 _('and'),
700 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
700 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
701 _('revisions')
701 _('revisions')
702 )
702 )
703 )
703 )
704
704
705 if not feed:
705 if not feed:
706 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
706 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
707 else:
707 else:
708 html_tmpl = '<span id="%s"> %s </span>'
708 html_tmpl = '<span id="%s"> %s </span>'
709
709
710 morelinks = ', '.join(
710 morelinks = ', '.join(
711 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
711 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
712 )
712 )
713
713
714 if len(revs_ids) > revs_top_limit:
714 if len(revs_ids) > revs_top_limit:
715 morelinks += ', ...'
715 morelinks += ', ...'
716
716
717 cs_links.append(html_tmpl % (uniq_id, morelinks))
717 cs_links.append(html_tmpl % (uniq_id, morelinks))
718 if len(revs) > 1:
718 if len(revs) > 1:
719 cs_links.append(compare_view)
719 cs_links.append(compare_view)
720 return ''.join(cs_links)
720 return ''.join(cs_links)
721
721
722 def get_fork_name():
722 def get_fork_name():
723 repo_name = action_params
723 repo_name = action_params
724 url_ = url('summary_home', repo_name=repo_name)
724 url_ = url('summary_home', repo_name=repo_name)
725 return _('Fork name %s') % link_to(action_params, url_)
725 return _('Fork name %s') % link_to(action_params, url_)
726
726
727 def get_user_name():
727 def get_user_name():
728 user_name = action_params
728 user_name = action_params
729 return user_name
729 return user_name
730
730
731 def get_users_group():
731 def get_users_group():
732 group_name = action_params
732 group_name = action_params
733 return group_name
733 return group_name
734
734
735 def get_pull_request():
735 def get_pull_request():
736 from kallithea.model.db import PullRequest
736 from kallithea.model.db import PullRequest
737 pull_request_id = action_params
737 pull_request_id = action_params
738 nice_id = PullRequest.make_nice_id(pull_request_id)
738 nice_id = PullRequest.make_nice_id(pull_request_id)
739
739
740 deleted = user_log.repository is None
740 deleted = user_log.repository is None
741 if deleted:
741 if deleted:
742 repo_name = user_log.repository_name
742 repo_name = user_log.repository_name
743 else:
743 else:
744 repo_name = user_log.repository.repo_name
744 repo_name = user_log.repository.repo_name
745
745
746 return link_to(_('Pull request %s') % nice_id,
746 return link_to(_('Pull request %s') % nice_id,
747 url('pullrequest_show', repo_name=repo_name,
747 url('pullrequest_show', repo_name=repo_name,
748 pull_request_id=pull_request_id))
748 pull_request_id=pull_request_id))
749
749
750 def get_archive_name():
750 def get_archive_name():
751 archive_name = action_params
751 archive_name = action_params
752 return archive_name
752 return archive_name
753
753
754 # action : translated str, callback(extractor), icon
754 # action : translated str, callback(extractor), icon
755 action_map = {
755 action_map = {
756 'user_deleted_repo': (_('[deleted] repository'),
756 'user_deleted_repo': (_('[deleted] repository'),
757 None, 'icon-trashcan'),
757 None, 'icon-trashcan'),
758 'user_created_repo': (_('[created] repository'),
758 'user_created_repo': (_('[created] repository'),
759 None, 'icon-plus'),
759 None, 'icon-plus'),
760 'user_created_fork': (_('[created] repository as fork'),
760 'user_created_fork': (_('[created] repository as fork'),
761 None, 'icon-fork'),
761 None, 'icon-fork'),
762 'user_forked_repo': (_('[forked] repository'),
762 'user_forked_repo': (_('[forked] repository'),
763 get_fork_name, 'icon-fork'),
763 get_fork_name, 'icon-fork'),
764 'user_updated_repo': (_('[updated] repository'),
764 'user_updated_repo': (_('[updated] repository'),
765 None, 'icon-pencil'),
765 None, 'icon-pencil'),
766 'user_downloaded_archive': (_('[downloaded] archive from repository'),
766 'user_downloaded_archive': (_('[downloaded] archive from repository'),
767 get_archive_name, 'icon-download-cloud'),
767 get_archive_name, 'icon-download-cloud'),
768 'admin_deleted_repo': (_('[delete] repository'),
768 'admin_deleted_repo': (_('[delete] repository'),
769 None, 'icon-trashcan'),
769 None, 'icon-trashcan'),
770 'admin_created_repo': (_('[created] repository'),
770 'admin_created_repo': (_('[created] repository'),
771 None, 'icon-plus'),
771 None, 'icon-plus'),
772 'admin_forked_repo': (_('[forked] repository'),
772 'admin_forked_repo': (_('[forked] repository'),
773 None, 'icon-fork'),
773 None, 'icon-fork'),
774 'admin_updated_repo': (_('[updated] repository'),
774 'admin_updated_repo': (_('[updated] repository'),
775 None, 'icon-pencil'),
775 None, 'icon-pencil'),
776 'admin_created_user': (_('[created] user'),
776 'admin_created_user': (_('[created] user'),
777 get_user_name, 'icon-user'),
777 get_user_name, 'icon-user'),
778 'admin_updated_user': (_('[updated] user'),
778 'admin_updated_user': (_('[updated] user'),
779 get_user_name, 'icon-user'),
779 get_user_name, 'icon-user'),
780 'admin_created_users_group': (_('[created] user group'),
780 'admin_created_users_group': (_('[created] user group'),
781 get_users_group, 'icon-pencil'),
781 get_users_group, 'icon-pencil'),
782 'admin_updated_users_group': (_('[updated] user group'),
782 'admin_updated_users_group': (_('[updated] user group'),
783 get_users_group, 'icon-pencil'),
783 get_users_group, 'icon-pencil'),
784 'user_commented_revision': (_('[commented] on revision in repository'),
784 'user_commented_revision': (_('[commented] on revision in repository'),
785 get_cs_links, 'icon-comment'),
785 get_cs_links, 'icon-comment'),
786 'user_commented_pull_request': (_('[commented] on pull request for'),
786 'user_commented_pull_request': (_('[commented] on pull request for'),
787 get_pull_request, 'icon-comment'),
787 get_pull_request, 'icon-comment'),
788 'user_closed_pull_request': (_('[closed] pull request for'),
788 'user_closed_pull_request': (_('[closed] pull request for'),
789 get_pull_request, 'icon-ok'),
789 get_pull_request, 'icon-ok'),
790 'push': (_('[pushed] into'),
790 'push': (_('[pushed] into'),
791 get_cs_links, 'icon-move-up'),
791 get_cs_links, 'icon-move-up'),
792 'push_local': (_('[committed via Kallithea] into repository'),
792 'push_local': (_('[committed via Kallithea] into repository'),
793 get_cs_links, 'icon-pencil'),
793 get_cs_links, 'icon-pencil'),
794 'push_remote': (_('[pulled from remote] into repository'),
794 'push_remote': (_('[pulled from remote] into repository'),
795 get_cs_links, 'icon-move-up'),
795 get_cs_links, 'icon-move-up'),
796 'pull': (_('[pulled] from'),
796 'pull': (_('[pulled] from'),
797 None, 'icon-move-down'),
797 None, 'icon-move-down'),
798 'started_following_repo': (_('[started following] repository'),
798 'started_following_repo': (_('[started following] repository'),
799 None, 'icon-heart'),
799 None, 'icon-heart'),
800 'stopped_following_repo': (_('[stopped following] repository'),
800 'stopped_following_repo': (_('[stopped following] repository'),
801 None, 'icon-heart-empty'),
801 None, 'icon-heart-empty'),
802 }
802 }
803
803
804 action_str = action_map.get(action, action)
804 action_str = action_map.get(action, action)
805 if feed:
805 if feed:
806 action = action_str[0].replace('[', '').replace(']', '')
806 action = action_str[0].replace('[', '').replace(']', '')
807 else:
807 else:
808 action = action_str[0] \
808 action = action_str[0] \
809 .replace('[', '<b>') \
809 .replace('[', '<b>') \
810 .replace(']', '</b>')
810 .replace(']', '</b>')
811
811
812 action_params_func = lambda: ""
812 action_params_func = lambda: ""
813
813
814 if callable(action_str[1]):
814 if callable(action_str[1]):
815 action_params_func = action_str[1]
815 action_params_func = action_str[1]
816
816
817 def action_parser_icon():
817 def action_parser_icon():
818 action = user_log.action
818 action = user_log.action
819 action_params = None
819 action_params = None
820 x = action.split(':')
820 x = action.split(':')
821
821
822 if len(x) > 1:
822 if len(x) > 1:
823 action, action_params = x
823 action, action_params = x
824
824
825 ico = action_map.get(action, ['', '', ''])[2]
825 ico = action_map.get(action, ['', '', ''])[2]
826 html = """<i class="%s"></i>""" % ico
826 html = """<i class="%s"></i>""" % ico
827 return literal(html)
827 return literal(html)
828
828
829 # returned callbacks we need to call to get
829 # returned callbacks we need to call to get
830 return [lambda: literal(action), action_params_func, action_parser_icon]
830 return [lambda: literal(action), action_params_func, action_parser_icon]
831
831
832
832
833
833
834 #==============================================================================
834 #==============================================================================
835 # PERMS
835 # PERMS
836 #==============================================================================
836 #==============================================================================
837 from kallithea.lib.auth import HasPermissionAny, \
837 from kallithea.lib.auth import HasPermissionAny, \
838 HasRepoPermissionLevel, HasRepoGroupPermissionLevel
838 HasRepoPermissionLevel, HasRepoGroupPermissionLevel
839
839
840
840
841 #==============================================================================
841 #==============================================================================
842 # GRAVATAR URL
842 # GRAVATAR URL
843 #==============================================================================
843 #==============================================================================
844 def gravatar_div(email_address, cls='', size=30, **div_attributes):
844 def gravatar_div(email_address, cls='', size=30, **div_attributes):
845 """Return an html literal with a span around a gravatar if they are enabled.
845 """Return an html literal with a span around a gravatar if they are enabled.
846 Extra keyword parameters starting with 'div_' will get the prefix removed
846 Extra keyword parameters starting with 'div_' will get the prefix removed
847 and '_' changed to '-' and be used as attributes on the div. The default
847 and '_' changed to '-' and be used as attributes on the div. The default
848 class is 'gravatar'.
848 class is 'gravatar'.
849 """
849 """
850 from tg import tmpl_context as c
850 from tg import tmpl_context as c
851 if not c.visual.use_gravatar:
851 if not c.visual.use_gravatar:
852 return ''
852 return ''
853 if 'div_class' not in div_attributes:
853 if 'div_class' not in div_attributes:
854 div_attributes['div_class'] = "gravatar"
854 div_attributes['div_class'] = "gravatar"
855 attributes = []
855 attributes = []
856 for k, v in sorted(div_attributes.items()):
856 for k, v in sorted(div_attributes.items()):
857 assert k.startswith('div_'), k
857 assert k.startswith('div_'), k
858 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
858 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
859 return literal("""<span%s>%s</span>""" %
859 return literal("""<span%s>%s</span>""" %
860 (''.join(attributes),
860 (''.join(attributes),
861 gravatar(email_address, cls=cls, size=size)))
861 gravatar(email_address, cls=cls, size=size)))
862
862
863
863
864 def gravatar(email_address, cls='', size=30):
864 def gravatar(email_address, cls='', size=30):
865 """return html element of the gravatar
865 """return html element of the gravatar
866
866
867 This method will return an <img> with the resolution double the size (for
867 This method will return an <img> with the resolution double the size (for
868 retina screens) of the image. If the url returned from gravatar_url is
868 retina screens) of the image. If the url returned from gravatar_url is
869 empty then we fallback to using an icon.
869 empty then we fallback to using an icon.
870
870
871 """
871 """
872 from tg import tmpl_context as c
872 from tg import tmpl_context as c
873 if not c.visual.use_gravatar:
873 if not c.visual.use_gravatar:
874 return ''
874 return ''
875
875
876 src = gravatar_url(email_address, size * 2)
876 src = gravatar_url(email_address, size * 2)
877
877
878 if src:
878 if src:
879 # here it makes sense to use style="width: ..." (instead of, say, a
879 # here it makes sense to use style="width: ..." (instead of, say, a
880 # stylesheet) because we using this to generate a high-res (retina) size
880 # stylesheet) because we using this to generate a high-res (retina) size
881 html = ('<i class="icon-gravatar {cls}"'
881 html = ('<i class="icon-gravatar {cls}"'
882 ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
882 ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
883 '></i>').format(cls=cls, size=size, src=src)
883 '></i>').format(cls=cls, size=size, src=src)
884
884
885 else:
885 else:
886 # if src is empty then there was no gravatar, so we use a font icon
886 # if src is empty then there was no gravatar, so we use a font icon
887 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
887 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
888 .format(cls=cls, size=size, src=src))
888 .format(cls=cls, size=size, src=src))
889
889
890 return literal(html)
890 return literal(html)
891
891
892
892
893 def gravatar_url(email_address, size=30, default=''):
893 def gravatar_url(email_address, size=30, default=''):
894 # doh, we need to re-import those to mock it later
894 # doh, we need to re-import those to mock it later
895 from kallithea.config.routing import url
895 from kallithea.config.routing import url
896 from kallithea.model.db import User
896 from kallithea.model.db import User
897 from tg import tmpl_context as c
897 from tg import tmpl_context as c
898 if not c.visual.use_gravatar:
898 if not c.visual.use_gravatar:
899 return ""
899 return ""
900
900
901 _def = 'anonymous@kallithea-scm.org' # default gravatar
901 _def = 'anonymous@kallithea-scm.org' # default gravatar
902 email_address = email_address or _def
902 email_address = email_address or _def
903
903
904 if email_address == _def:
904 if email_address == _def:
905 return default
905 return default
906
906
907 parsed_url = urlparse.urlparse(url.current(qualified=True))
907 parsed_url = urlparse.urlparse(url.current(qualified=True))
908 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
908 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
909 .replace('{email}', email_address) \
909 .replace('{email}', email_address) \
910 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
910 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
911 .replace('{netloc}', parsed_url.netloc) \
911 .replace('{netloc}', parsed_url.netloc) \
912 .replace('{scheme}', parsed_url.scheme) \
912 .replace('{scheme}', parsed_url.scheme) \
913 .replace('{size}', safe_str(size))
913 .replace('{size}', safe_str(size))
914 return url
914 return url
915
915
916
916
917 def changed_tooltip(nodes):
917 def changed_tooltip(nodes):
918 """
918 """
919 Generates a html string for changed nodes in changeset page.
919 Generates a html string for changed nodes in changeset page.
920 It limits the output to 30 entries
920 It limits the output to 30 entries
921
921
922 :param nodes: LazyNodesGenerator
922 :param nodes: LazyNodesGenerator
923 """
923 """
924 if nodes:
924 if nodes:
925 pref = ': <br/> '
925 pref = ': <br/> '
926 suf = ''
926 suf = ''
927 if len(nodes) > 30:
927 if len(nodes) > 30:
928 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
928 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
929 return literal(pref + '<br/> '.join([safe_unicode(x.path)
929 return literal(pref + '<br/> '.join([safe_unicode(x.path)
930 for x in nodes[:30]]) + suf)
930 for x in nodes[:30]]) + suf)
931 else:
931 else:
932 return ': ' + _('No files')
932 return ': ' + _('No files')
933
933
934
934
935 def fancy_file_stats(stats):
935 def fancy_file_stats(stats):
936 """
936 """
937 Displays a fancy two colored bar for number of added/deleted
937 Displays a fancy two colored bar for number of added/deleted
938 lines of code on file
938 lines of code on file
939
939
940 :param stats: two element list of added/deleted lines of code
940 :param stats: two element list of added/deleted lines of code
941 """
941 """
942 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
942 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
943 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
943 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
944
944
945 a, d = stats['added'], stats['deleted']
945 a, d = stats['added'], stats['deleted']
946 width = 100
946 width = 100
947
947
948 if stats['binary']:
948 if stats['binary']:
949 # binary mode
949 # binary mode
950 lbl = ''
950 lbl = ''
951 bin_op = 1
951 bin_op = 1
952
952
953 if BIN_FILENODE in stats['ops']:
953 if BIN_FILENODE in stats['ops']:
954 lbl = 'bin+'
954 lbl = 'bin+'
955
955
956 if NEW_FILENODE in stats['ops']:
956 if NEW_FILENODE in stats['ops']:
957 lbl += _('new file')
957 lbl += _('new file')
958 bin_op = NEW_FILENODE
958 bin_op = NEW_FILENODE
959 elif MOD_FILENODE in stats['ops']:
959 elif MOD_FILENODE in stats['ops']:
960 lbl += _('mod')
960 lbl += _('mod')
961 bin_op = MOD_FILENODE
961 bin_op = MOD_FILENODE
962 elif DEL_FILENODE in stats['ops']:
962 elif DEL_FILENODE in stats['ops']:
963 lbl += _('del')
963 lbl += _('del')
964 bin_op = DEL_FILENODE
964 bin_op = DEL_FILENODE
965 elif RENAMED_FILENODE in stats['ops']:
965 elif RENAMED_FILENODE in stats['ops']:
966 lbl += _('rename')
966 lbl += _('rename')
967 bin_op = RENAMED_FILENODE
967 bin_op = RENAMED_FILENODE
968
968
969 # chmod can go with other operations
969 # chmod can go with other operations
970 if CHMOD_FILENODE in stats['ops']:
970 if CHMOD_FILENODE in stats['ops']:
971 _org_lbl = _('chmod')
971 _org_lbl = _('chmod')
972 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
972 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
973
973
974 #import ipdb;ipdb.set_trace()
974 #import ipdb;ipdb.set_trace()
975 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
975 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
976 b_a = '<div class="bin bin1" style="width:0%"></div>'
976 b_a = '<div class="bin bin1" style="width:0%"></div>'
977 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
977 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
978
978
979 t = stats['added'] + stats['deleted']
979 t = stats['added'] + stats['deleted']
980 unit = float(width) / (t or 1)
980 unit = float(width) / (t or 1)
981
981
982 # needs > 9% of width to be visible or 0 to be hidden
982 # needs > 9% of width to be visible or 0 to be hidden
983 a_p = max(9, unit * a) if a > 0 else 0
983 a_p = max(9, unit * a) if a > 0 else 0
984 d_p = max(9, unit * d) if d > 0 else 0
984 d_p = max(9, unit * d) if d > 0 else 0
985 p_sum = a_p + d_p
985 p_sum = a_p + d_p
986
986
987 if p_sum > width:
987 if p_sum > width:
988 # adjust the percentage to be == 100% since we adjusted to 9
988 # adjust the percentage to be == 100% since we adjusted to 9
989 if a_p > d_p:
989 if a_p > d_p:
990 a_p = a_p - (p_sum - width)
990 a_p = a_p - (p_sum - width)
991 else:
991 else:
992 d_p = d_p - (p_sum - width)
992 d_p = d_p - (p_sum - width)
993
993
994 a_v = a if a > 0 else ''
994 a_v = a if a > 0 else ''
995 d_v = d if d > 0 else ''
995 d_v = d if d > 0 else ''
996
996
997 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
997 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
998 a_p, a_v
998 a_p, a_v
999 )
999 )
1000 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
1000 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
1001 d_p, d_v
1001 d_p, d_v
1002 )
1002 )
1003 return literal('<div class="progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1003 return literal('<div class="progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1004
1004
1005
1005
1006 _URLIFY_RE = re.compile(r'''
1006 _URLIFY_RE = re.compile(r'''
1007 # URL markup
1007 # URL markup
1008 (?P<url>%s) |
1008 (?P<url>%s) |
1009 # @mention markup
1009 # @mention markup
1010 (?P<mention>%s) |
1010 (?P<mention>%s) |
1011 # Changeset hash markup
1011 # Changeset hash markup
1012 (?<!\w|[-_])
1012 (?<!\w|[-_])
1013 (?P<hash>[0-9a-f]{12,40})
1013 (?P<hash>[0-9a-f]{12,40})
1014 (?!\w|[-_]) |
1014 (?!\w|[-_]) |
1015 # Markup of *bold text*
1015 # Markup of *bold text*
1016 (?:
1016 (?:
1017 (?:^|(?<=\s))
1017 (?:^|(?<=\s))
1018 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
1018 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
1019 (?![*\w])
1019 (?![*\w])
1020 ) |
1020 ) |
1021 # "Stylize" markup
1021 # "Stylize" markup
1022 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1022 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1023 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1023 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
1024 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
1024 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
1025 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
1025 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
1026 \[(?P<tag>[a-z]+)\]
1026 \[(?P<tag>[a-z]+)\]
1027 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
1027 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
1028 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1028 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
1029
1029
1030
1030
1031 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1031 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
1032 """
1032 """
1033 Parses given text message and make literal html with markup.
1033 Parses given text message and make literal html with markup.
1034 The text will be truncated to the specified length.
1034 The text will be truncated to the specified length.
1035 Hashes are turned into changeset links to specified repository.
1035 Hashes are turned into changeset links to specified repository.
1036 URLs links to what they say.
1036 URLs links to what they say.
1037 Issues are linked to given issue-server.
1037 Issues are linked to given issue-server.
1038 If link_ is provided, all text not already linking somewhere will link there.
1038 If link_ is provided, all text not already linking somewhere will link there.
1039 """
1039 """
1040
1040
1041 def _replace(match_obj):
1041 def _replace(match_obj):
1042 url = match_obj.group('url')
1042 url = match_obj.group('url')
1043 if url is not None:
1043 if url is not None:
1044 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1044 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
1045 mention = match_obj.group('mention')
1045 mention = match_obj.group('mention')
1046 if mention is not None:
1046 if mention is not None:
1047 return '<b>%s</b>' % mention
1047 return '<b>%s</b>' % mention
1048 hash_ = match_obj.group('hash')
1048 hash_ = match_obj.group('hash')
1049 if hash_ is not None and repo_name is not None:
1049 if hash_ is not None and repo_name is not None:
1050 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
1050 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
1051 return '<a class="changeset_hash" href="%(url)s">%(hash)s</a>' % {
1051 return '<a class="changeset_hash" href="%(url)s">%(hash)s</a>' % {
1052 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
1052 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
1053 'hash': hash_,
1053 'hash': hash_,
1054 }
1054 }
1055 bold = match_obj.group('bold')
1055 bold = match_obj.group('bold')
1056 if bold is not None:
1056 if bold is not None:
1057 return '<b>*%s*</b>' % _urlify(bold[1:-1])
1057 return '<b>*%s*</b>' % _urlify(bold[1:-1])
1058 if stylize:
1058 if stylize:
1059 seen = match_obj.group('seen')
1059 seen = match_obj.group('seen')
1060 if seen:
1060 if seen:
1061 return '<div class="label label-meta" data-tag="see">see =&gt; %s</div>' % seen
1061 return '<div class="label label-meta" data-tag="see">see =&gt; %s</div>' % seen
1062 license = match_obj.group('license')
1062 license = match_obj.group('license')
1063 if license:
1063 if license:
1064 return '<div class="label label-meta" data-tag="license"><a href="http:\/\/www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1064 return '<div class="label label-meta" data-tag="license"><a href="http:\/\/www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1065 tagtype = match_obj.group('tagtype')
1065 tagtype = match_obj.group('tagtype')
1066 if tagtype:
1066 if tagtype:
1067 tagvalue = match_obj.group('tagvalue')
1067 tagvalue = match_obj.group('tagvalue')
1068 return '<div class="label label-meta" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1068 return '<div class="label label-meta" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1069 lang = match_obj.group('lang')
1069 lang = match_obj.group('lang')
1070 if lang:
1070 if lang:
1071 return '<div class="label label-meta" data-tag="lang">%s</div>' % lang
1071 return '<div class="label label-meta" data-tag="lang">%s</div>' % lang
1072 tag = match_obj.group('tag')
1072 tag = match_obj.group('tag')
1073 if tag:
1073 if tag:
1074 return '<div class="label label-meta" data-tag="%s">%s</div>' % (tag, tag)
1074 return '<div class="label label-meta" data-tag="%s">%s</div>' % (tag, tag)
1075 return match_obj.group(0)
1075 return match_obj.group(0)
1076
1076
1077 def _urlify(s):
1077 def _urlify(s):
1078 """
1078 """
1079 Extract urls from text and make html links out of them
1079 Extract urls from text and make html links out of them
1080 """
1080 """
1081 return _URLIFY_RE.sub(_replace, s)
1081 return _URLIFY_RE.sub(_replace, s)
1082
1082
1083 if truncate is None:
1083 if truncate is None:
1084 s = s.rstrip()
1084 s = s.rstrip()
1085 else:
1085 else:
1086 s = truncatef(s, truncate, whole_word=True)
1086 s = truncatef(s, truncate, whole_word=True)
1087 s = html_escape(s)
1087 s = html_escape(s)
1088 s = _urlify(s)
1088 s = _urlify(s)
1089 if repo_name is not None:
1089 if repo_name is not None:
1090 s = urlify_issues(s, repo_name)
1090 s = urlify_issues(s, repo_name)
1091 if link_ is not None:
1091 if link_ is not None:
1092 # make href around everything that isn't a href already
1092 # make href around everything that isn't a href already
1093 s = linkify_others(s, link_)
1093 s = linkify_others(s, link_)
1094 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1094 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1095 # Turn HTML5 into more valid HTML4 as required by some mail readers.
1095 # Turn HTML5 into more valid HTML4 as required by some mail readers.
1096 # (This is not done in one step in html_escape, because character codes like
1096 # (This is not done in one step in html_escape, because character codes like
1097 # &#123; risk to be seen as an issue reference due to the presence of '#'.)
1097 # &#123; risk to be seen as an issue reference due to the presence of '#'.)
1098 s = s.replace("&apos;", "&#39;")
1098 s = s.replace("&apos;", "&#39;")
1099 return literal(s)
1099 return literal(s)
1100
1100
1101
1101
1102 def linkify_others(t, l):
1102 def linkify_others(t, l):
1103 """Add a default link to html with links.
1103 """Add a default link to html with links.
1104 HTML doesn't allow nesting of links, so the outer link must be broken up
1104 HTML doesn't allow nesting of links, so the outer link must be broken up
1105 in pieces and give space for other links.
1105 in pieces and give space for other links.
1106 """
1106 """
1107 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1107 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1108 links = []
1108 links = []
1109 for e in urls.split(t):
1109 for e in urls.split(t):
1110 if e.strip() and not urls.match(e):
1110 if e.strip() and not urls.match(e):
1111 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1111 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1112 else:
1112 else:
1113 links.append(e)
1113 links.append(e)
1114
1114
1115 return ''.join(links)
1115 return ''.join(links)
1116
1116
1117
1117
1118 # Global variable that will hold the actual urlify_issues function body.
1118 # Global variable that will hold the actual urlify_issues function body.
1119 # Will be set on first use when the global configuration has been read.
1119 # Will be set on first use when the global configuration has been read.
1120 _urlify_issues_f = None
1120 _urlify_issues_f = None
1121
1121
1122
1122
1123 def urlify_issues(newtext, repo_name):
1123 def urlify_issues(newtext, repo_name):
1124 """Urlify issue references according to .ini configuration"""
1124 """Urlify issue references according to .ini configuration"""
1125 global _urlify_issues_f
1125 global _urlify_issues_f
1126 if _urlify_issues_f is None:
1126 if _urlify_issues_f is None:
1127 from kallithea import CONFIG
1127 from kallithea import CONFIG
1128 from kallithea.model.db import URL_SEP
1128 from kallithea.model.db import URL_SEP
1129 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1129 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1130
1130
1131 # Build chain of urlify functions, starting with not doing any transformation
1131 # Build chain of urlify functions, starting with not doing any transformation
1132 tmp_urlify_issues_f = lambda s: s
1132 tmp_urlify_issues_f = lambda s: s
1133
1133
1134 issue_pat_re = re.compile(r'issue_pat(.*)')
1134 issue_pat_re = re.compile(r'issue_pat(.*)')
1135 for k in CONFIG.keys():
1135 for k in CONFIG.keys():
1136 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1136 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1137 m = issue_pat_re.match(k)
1137 m = issue_pat_re.match(k)
1138 if m is None:
1138 if m is None:
1139 continue
1139 continue
1140 suffix = m.group(1)
1140 suffix = m.group(1)
1141 issue_pat = CONFIG.get(k)
1141 issue_pat = CONFIG.get(k)
1142 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1142 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1143 issue_sub = CONFIG.get('issue_sub%s' % suffix)
1143 issue_sub = CONFIG.get('issue_sub%s' % suffix)
1144 if not issue_pat or not issue_server_link or issue_sub is None: # issue_sub can be empty but should be present
1144 if not issue_pat or not issue_server_link or issue_sub is None: # issue_sub can be empty but should be present
1145 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1145 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1146 continue
1146 continue
1147
1147
1148 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1148 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1149 try:
1149 try:
1150 issue_re = re.compile(issue_pat)
1150 issue_re = re.compile(issue_pat)
1151 except re.error as e:
1151 except re.error as e:
1152 log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', suffix, issue_pat, issue_server_link, issue_sub, str(e))
1152 log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', suffix, issue_pat, issue_server_link, issue_sub, str(e))
1153 continue
1153 continue
1154
1154
1155 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1155 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
1156
1156
1157 def issues_replace(match_obj,
1157 def issues_replace(match_obj,
1158 issue_server_link=issue_server_link, issue_sub=issue_sub):
1158 issue_server_link=issue_server_link, issue_sub=issue_sub):
1159 try:
1159 try:
1160 issue_url = match_obj.expand(issue_server_link)
1160 issue_url = match_obj.expand(issue_server_link)
1161 except (IndexError, re.error) as e:
1161 except (IndexError, re.error) as e:
1162 log.error('invalid issue_url setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1162 log.error('invalid issue_url setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1163 issue_url = issue_server_link
1163 issue_url = issue_server_link
1164 issue_url = issue_url.replace('{repo}', repo_name)
1164 issue_url = issue_url.replace('{repo}', repo_name)
1165 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1165 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1166 # if issue_sub is empty use the matched issue reference verbatim
1166 # if issue_sub is empty use the matched issue reference verbatim
1167 if not issue_sub:
1167 if not issue_sub:
1168 issue_text = match_obj.group()
1168 issue_text = match_obj.group()
1169 else:
1169 else:
1170 try:
1170 try:
1171 issue_text = match_obj.expand(issue_sub)
1171 issue_text = match_obj.expand(issue_sub)
1172 except (IndexError, re.error) as e:
1172 except (IndexError, re.error) as e:
1173 log.error('invalid issue_sub setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1173 log.error('invalid issue_sub setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1174 issue_text = match_obj.group()
1174 issue_text = match_obj.group()
1175
1175
1176 return (
1176 return (
1177 '<a class="issue-tracker-link" href="%(url)s">'
1177 '<a class="issue-tracker-link" href="%(url)s">'
1178 '%(text)s'
1178 '%(text)s'
1179 '</a>'
1179 '</a>'
1180 ) % {
1180 ) % {
1181 'url': issue_url,
1181 'url': issue_url,
1182 'text': issue_text,
1182 'text': issue_text,
1183 }
1183 }
1184 tmp_urlify_issues_f = (lambda s,
1184 tmp_urlify_issues_f = (lambda s,
1185 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1185 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1186 issue_re.sub(issues_replace, chain_f(s)))
1186 issue_re.sub(issues_replace, chain_f(s)))
1187
1187
1188 # Set tmp function globally - atomically
1188 # Set tmp function globally - atomically
1189 _urlify_issues_f = tmp_urlify_issues_f
1189 _urlify_issues_f = tmp_urlify_issues_f
1190
1190
1191 return _urlify_issues_f(newtext)
1191 return _urlify_issues_f(newtext)
1192
1192
1193
1193
1194 def render_w_mentions(source, repo_name=None):
1194 def render_w_mentions(source, repo_name=None):
1195 """
1195 """
1196 Render plain text with revision hashes and issue references urlified
1196 Render plain text with revision hashes and issue references urlified
1197 and with @mention highlighting.
1197 and with @mention highlighting.
1198 """
1198 """
1199 s = safe_unicode(source)
1199 s = safe_unicode(source)
1200 s = urlify_text(s, repo_name=repo_name)
1200 s = urlify_text(s, repo_name=repo_name)
1201 return literal('<div class="formatted-fixed">%s</div>' % s)
1201 return literal('<div class="formatted-fixed">%s</div>' % s)
1202
1202
1203
1203
1204 def short_ref(ref_type, ref_name):
1204 def short_ref(ref_type, ref_name):
1205 if ref_type == 'rev':
1205 if ref_type == 'rev':
1206 return short_id(ref_name)
1206 return short_id(ref_name)
1207 return ref_name
1207 return ref_name
1208
1208
1209
1209
1210 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1210 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1211 """
1211 """
1212 Return full markup for a href to changeset_home for a changeset.
1212 Return full markup for a href to changeset_home for a changeset.
1213 If ref_type is branch it will link to changelog.
1213 If ref_type is branch it will link to changelog.
1214 ref_name is shortened if ref_type is 'rev'.
1214 ref_name is shortened if ref_type is 'rev'.
1215 if rev is specified show it too, explicitly linking to that revision.
1215 if rev is specified show it too, explicitly linking to that revision.
1216 """
1216 """
1217 txt = short_ref(ref_type, ref_name)
1217 txt = short_ref(ref_type, ref_name)
1218 if ref_type == 'branch':
1218 if ref_type == 'branch':
1219 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1219 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1220 else:
1220 else:
1221 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1221 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1222 l = link_to(repo_name + '#' + txt, u)
1222 l = link_to(repo_name + '#' + txt, u)
1223 if rev and ref_type != 'rev':
1223 if rev and ref_type != 'rev':
1224 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1224 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1225 return l
1225 return l
1226
1226
1227
1227
1228 def changeset_status(repo, revision):
1228 def changeset_status(repo, revision):
1229 from kallithea.model.changeset_status import ChangesetStatusModel
1229 from kallithea.model.changeset_status import ChangesetStatusModel
1230 return ChangesetStatusModel().get_status(repo, revision)
1230 return ChangesetStatusModel().get_status(repo, revision)
1231
1231
1232
1232
1233 def changeset_status_lbl(changeset_status):
1233 def changeset_status_lbl(changeset_status):
1234 from kallithea.model.db import ChangesetStatus
1234 from kallithea.model.db import ChangesetStatus
1235 return ChangesetStatus.get_status_lbl(changeset_status)
1235 return ChangesetStatus.get_status_lbl(changeset_status)
1236
1236
1237
1237
1238 def get_permission_name(key):
1238 def get_permission_name(key):
1239 from kallithea.model.db import Permission
1239 from kallithea.model.db import Permission
1240 return dict(Permission.PERMS).get(key)
1240 return dict(Permission.PERMS).get(key)
1241
1241
1242
1242
1243 def journal_filter_help():
1243 def journal_filter_help():
1244 return _(textwrap.dedent('''
1244 return _(textwrap.dedent('''
1245 Example filter terms:
1245 Example filter terms:
1246 repository:vcs
1246 repository:vcs
1247 username:developer
1247 username:developer
1248 action:*push*
1248 action:*push*
1249 ip:127.0.0.1
1249 ip:127.0.0.1
1250 date:20120101
1250 date:20120101
1251 date:[20120101100000 TO 20120102]
1251 date:[20120101100000 TO 20120102]
1252
1252
1253 Generate wildcards using '*' character:
1253 Generate wildcards using '*' character:
1254 "repository:vcs*" - search everything starting with 'vcs'
1254 "repository:vcs*" - search everything starting with 'vcs'
1255 "repository:*vcs*" - search for repository containing 'vcs'
1255 "repository:*vcs*" - search for repository containing 'vcs'
1256
1256
1257 Optional AND / OR operators in queries
1257 Optional AND / OR operators in queries
1258 "repository:vcs OR repository:test"
1258 "repository:vcs OR repository:test"
1259 "username:test AND repository:test*"
1259 "username:test AND repository:test*"
1260 '''))
1260 '''))
1261
1261
1262
1262
1263 def not_mapped_error(repo_name):
1263 def not_mapped_error(repo_name):
1264 flash(_('%s repository is not mapped to db perhaps'
1264 flash(_('%s repository is not mapped to db perhaps'
1265 ' it was created or renamed from the filesystem'
1265 ' it was created or renamed from the filesystem'
1266 ' please run the application again'
1266 ' please run the application again'
1267 ' in order to rescan repositories') % repo_name, category='error')
1267 ' in order to rescan repositories') % repo_name, category='error')
1268
1268
1269
1269
1270 def ip_range(ip_addr):
1270 def ip_range(ip_addr):
1271 from kallithea.model.db import UserIpMap
1271 from kallithea.model.db import UserIpMap
1272 s, e = UserIpMap._get_ip_range(ip_addr)
1272 s, e = UserIpMap._get_ip_range(ip_addr)
1273 return '%s - %s' % (s, e)
1273 return '%s - %s' % (s, e)
1274
1274
1275
1275
1276 def form(url, method="post", **attrs):
1276 def form(url, method="post", **attrs):
1277 """Like webhelpers.html.tags.form but automatically using secure_form with
1277 """Like webhelpers.html.tags.form but automatically using secure_form with
1278 authentication_token for POST. authentication_token is thus never leaked
1278 authentication_token for POST. authentication_token is thus never leaked
1279 in the URL."""
1279 in the URL."""
1280 if method.lower() == 'get':
1280 if method.lower() == 'get':
1281 return insecure_form(url, method=method, **attrs)
1281 return insecure_form(url, method=method, **attrs)
1282 # webhelpers will turn everything but GET into POST
1282 # webhelpers will turn everything but GET into POST
1283 return secure_form(url, method=method, **attrs)
1283 return secure_form(url, method=method, **attrs)
@@ -1,230 +1,230 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.middleware.pygrack
15 kallithea.lib.middleware.pygrack
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Python implementation of git-http-backend's Smart HTTP protocol
18 Python implementation of git-http-backend's Smart HTTP protocol
19
19
20 Based on original code from git_http_backend.py project.
20 Based on original code from git_http_backend.py project.
21
21
22 Copyright (c) 2010 Daniel Dotsenko <dotsa@hotmail.com>
22 Copyright (c) 2010 Daniel Dotsenko <dotsa@hotmail.com>
23 Copyright (c) 2012 Marcin Kuzminski <marcin@python-works.com>
23 Copyright (c) 2012 Marcin Kuzminski <marcin@python-works.com>
24
24
25 This file was forked by the Kallithea project in July 2014.
25 This file was forked by the Kallithea project in July 2014.
26 """
26 """
27
27
28 import os
28 import os
29 import socket
29 import socket
30 import logging
30 import logging
31 import traceback
31 import traceback
32
32
33 from webob import Request, Response, exc
33 from webob import Request, Response, exc
34
34
35 import kallithea
35 import kallithea
36 from kallithea.lib.vcs import subprocessio
36 from kallithea.lib.vcs import subprocessio
37 from kallithea.lib.utils2 import safe_unicode
37 from kallithea.lib.utils2 import safe_unicode
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class FileWrapper(object):
42 class FileWrapper(object):
43
43
44 def __init__(self, fd, content_length):
44 def __init__(self, fd, content_length):
45 self.fd = fd
45 self.fd = fd
46 self.content_length = content_length
46 self.content_length = content_length
47 self.remain = content_length
47 self.remain = content_length
48
48
49 def read(self, size):
49 def read(self, size):
50 if size <= self.remain:
50 if size <= self.remain:
51 try:
51 try:
52 data = self.fd.read(size)
52 data = self.fd.read(size)
53 except socket.error:
53 except socket.error:
54 raise IOError(self)
54 raise IOError(self)
55 self.remain -= size
55 self.remain -= size
56 elif self.remain:
56 elif self.remain:
57 data = self.fd.read(self.remain)
57 data = self.fd.read(self.remain)
58 self.remain = 0
58 self.remain = 0
59 else:
59 else:
60 data = None
60 data = None
61 return data
61 return data
62
62
63 def __repr__(self):
63 def __repr__(self):
64 return '<FileWrapper %s len: %s, read: %s>' % (
64 return '<FileWrapper %s len: %s, read: %s>' % (
65 self.fd, self.content_length, self.content_length - self.remain
65 self.fd, self.content_length, self.content_length - self.remain
66 )
66 )
67
67
68
68
69 class GitRepository(object):
69 class GitRepository(object):
70 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
70 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
71 commands = ['git-upload-pack', 'git-receive-pack']
71 commands = ['git-upload-pack', 'git-receive-pack']
72
72
73 def __init__(self, repo_name, content_path, extras):
73 def __init__(self, repo_name, content_path, extras):
74 files = set([f.lower() for f in os.listdir(content_path)])
74 files = set([f.lower() for f in os.listdir(content_path)])
75 if not (self.git_folder_signature.intersection(files)
75 if not (self.git_folder_signature.intersection(files)
76 == self.git_folder_signature):
76 == self.git_folder_signature):
77 raise OSError('%s missing git signature' % content_path)
77 raise OSError('%s missing git signature' % content_path)
78 self.content_path = content_path
78 self.content_path = content_path
79 self.valid_accepts = ['application/x-%s-result' %
79 self.valid_accepts = ['application/x-%s-result' %
80 c for c in self.commands]
80 c for c in self.commands]
81 self.repo_name = repo_name
81 self.repo_name = repo_name
82 self.extras = extras
82 self.extras = extras
83
83
84 def _get_fixedpath(self, path):
84 def _get_fixedpath(self, path):
85 """
85 """
86 Small fix for repo_path
86 Small fix for repo_path
87
87
88 :param path:
88 :param path:
89 """
89 """
90 path = safe_unicode(path)
90 path = safe_unicode(path)
91 assert path.startswith('/' + self.repo_name + '/')
91 assert path.startswith('/' + self.repo_name + '/')
92 return path[len(self.repo_name) + 2:].strip('/')
92 return path[len(self.repo_name) + 2:].strip('/')
93
93
94 def inforefs(self, req, environ):
94 def inforefs(self, req, environ):
95 """
95 """
96 WSGI Response producer for HTTP GET Git Smart
96 WSGI Response producer for HTTP GET Git Smart
97 HTTP /info/refs request.
97 HTTP /info/refs request.
98 """
98 """
99
99
100 git_command = req.GET.get('service')
100 git_command = req.GET.get('service')
101 if git_command not in self.commands:
101 if git_command not in self.commands:
102 log.debug('command %s not allowed', git_command)
102 log.debug('command %s not allowed', git_command)
103 return exc.HTTPMethodNotAllowed()
103 return exc.HTTPMethodNotAllowed()
104
104
105 # From Documentation/technical/http-protocol.txt shipped with Git:
105 # From Documentation/technical/http-protocol.txt shipped with Git:
106 #
106 #
107 # Clients MUST verify the first pkt-line is `# service=$servicename`.
107 # Clients MUST verify the first pkt-line is `# service=$servicename`.
108 # Servers MUST set $servicename to be the request parameter value.
108 # Servers MUST set $servicename to be the request parameter value.
109 # Servers SHOULD include an LF at the end of this line.
109 # Servers SHOULD include an LF at the end of this line.
110 # Clients MUST ignore an LF at the end of the line.
110 # Clients MUST ignore an LF at the end of the line.
111 #
111 #
112 # smart_reply = PKT-LINE("# service=$servicename" LF)
112 # smart_reply = PKT-LINE("# service=$servicename" LF)
113 # ref_list
113 # ref_list
114 # "0000"
114 # "0000"
115 server_advert = '# service=%s\n' % git_command
115 server_advert = '# service=%s\n' % git_command
116 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
116 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
117 _git_path = kallithea.CONFIG.get('git_path', 'git')
117 _git_path = kallithea.CONFIG.get('git_path', 'git')
118 cmd = [_git_path, git_command[4:],
118 cmd = [_git_path, git_command[4:],
119 '--stateless-rpc', '--advertise-refs', self.content_path]
119 '--stateless-rpc', '--advertise-refs', self.content_path]
120 log.debug('handling cmd %s', cmd)
120 log.debug('handling cmd %s', cmd)
121 try:
121 try:
122 out = subprocessio.SubprocessIOChunker(cmd,
122 out = subprocessio.SubprocessIOChunker(cmd,
123 starting_values=[packet_len + server_advert + '0000']
123 starting_values=[packet_len + server_advert + '0000']
124 )
124 )
125 except EnvironmentError as e:
125 except EnvironmentError as e:
126 log.error(traceback.format_exc())
126 log.error(traceback.format_exc())
127 raise exc.HTTPExpectationFailed()
127 raise exc.HTTPExpectationFailed()
128 resp = Response()
128 resp = Response()
129 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
129 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
130 resp.charset = None
130 resp.charset = None
131 resp.app_iter = out
131 resp.app_iter = out
132 return resp
132 return resp
133
133
134 def backend(self, req, environ):
134 def backend(self, req, environ):
135 """
135 """
136 WSGI Response producer for HTTP POST Git Smart HTTP requests.
136 WSGI Response producer for HTTP POST Git Smart HTTP requests.
137 Reads commands and data from HTTP POST's body.
137 Reads commands and data from HTTP POST's body.
138 returns an iterator obj with contents of git command's
138 returns an iterator obj with contents of git command's
139 response to stdout
139 response to stdout
140 """
140 """
141 _git_path = kallithea.CONFIG.get('git_path', 'git')
141 _git_path = kallithea.CONFIG.get('git_path', 'git')
142 git_command = self._get_fixedpath(req.path_info)
142 git_command = self._get_fixedpath(req.path_info)
143 if git_command not in self.commands:
143 if git_command not in self.commands:
144 log.debug('command %s not allowed', git_command)
144 log.debug('command %s not allowed', git_command)
145 return exc.HTTPMethodNotAllowed()
145 return exc.HTTPMethodNotAllowed()
146
146
147 if 'CONTENT_LENGTH' in environ:
147 if 'CONTENT_LENGTH' in environ:
148 inputstream = FileWrapper(environ['wsgi.input'],
148 inputstream = FileWrapper(environ['wsgi.input'],
149 req.content_length)
149 req.content_length)
150 else:
150 else:
151 inputstream = environ['wsgi.input']
151 inputstream = environ['wsgi.input']
152
152
153 gitenv = dict(os.environ)
153 gitenv = dict(os.environ)
154 # forget all configs
154 # forget all configs
155 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
155 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
156 cmd = [_git_path, git_command[4:], '--stateless-rpc', self.content_path]
156 cmd = [_git_path, git_command[4:], '--stateless-rpc', self.content_path]
157 log.debug('handling cmd %s', cmd)
157 log.debug('handling cmd %s', cmd)
158 try:
158 try:
159 out = subprocessio.SubprocessIOChunker(
159 out = subprocessio.SubprocessIOChunker(
160 cmd,
160 cmd,
161 inputstream=inputstream,
161 inputstream=inputstream,
162 env=gitenv,
162 env=gitenv,
163 cwd=self.content_path,
163 cwd=self.content_path,
164 )
164 )
165 except EnvironmentError as e:
165 except EnvironmentError as e:
166 log.error(traceback.format_exc())
166 log.error(traceback.format_exc())
167 raise exc.HTTPExpectationFailed()
167 raise exc.HTTPExpectationFailed()
168
168
169 if git_command in [u'git-receive-pack']:
169 if git_command in [u'git-receive-pack']:
170 # updating refs manually after each push.
170 # updating refs manually after each push.
171 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
171 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
172 from kallithea.lib.vcs import get_repo
172 from kallithea.lib.vcs import get_repo
173 from dulwich.server import update_server_info
173 from dulwich.server import update_server_info
174 repo = get_repo(self.content_path)
174 repo = get_repo(self.content_path)
175 if repo:
175 if repo:
176 update_server_info(repo._repo)
176 update_server_info(repo._repo)
177
177
178 resp = Response()
178 resp = Response()
179 resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
179 resp.content_type = 'application/x-%s-result' % git_command.encode('utf-8')
180 resp.charset = None
180 resp.charset = None
181 resp.app_iter = out
181 resp.app_iter = out
182 return resp
182 return resp
183
183
184 def __call__(self, environ, start_response):
184 def __call__(self, environ, start_response):
185 req = Request(environ)
185 req = Request(environ)
186 _path = self._get_fixedpath(req.path_info)
186 _path = self._get_fixedpath(req.path_info)
187 if _path.startswith('info/refs'):
187 if _path.startswith('info/refs'):
188 app = self.inforefs
188 app = self.inforefs
189 elif [a for a in self.valid_accepts if a in req.accept]:
189 elif [a for a in self.valid_accepts if a in req.accept]:
190 app = self.backend
190 app = self.backend
191 try:
191 try:
192 resp = app(req, environ)
192 resp = app(req, environ)
193 except exc.HTTPException as e:
193 except exc.HTTPException as e:
194 resp = e
194 resp = e
195 log.error(traceback.format_exc())
195 log.error(traceback.format_exc())
196 except Exception as e:
196 except Exception as e:
197 log.error(traceback.format_exc())
197 log.error(traceback.format_exc())
198 resp = exc.HTTPInternalServerError()
198 resp = exc.HTTPInternalServerError()
199 return resp(environ, start_response)
199 return resp(environ, start_response)
200
200
201
201
202 class GitDirectory(object):
202 class GitDirectory(object):
203
203
204 def __init__(self, repo_root, repo_name, extras):
204 def __init__(self, repo_root, repo_name, extras):
205 repo_location = os.path.join(repo_root, repo_name)
205 repo_location = os.path.join(repo_root, repo_name)
206 if not os.path.isdir(repo_location):
206 if not os.path.isdir(repo_location):
207 raise OSError(repo_location)
207 raise OSError(repo_location)
208
208
209 self.content_path = repo_location
209 self.content_path = repo_location
210 self.repo_name = repo_name
210 self.repo_name = repo_name
211 self.repo_location = repo_location
211 self.repo_location = repo_location
212 self.extras = extras
212 self.extras = extras
213
213
214 def __call__(self, environ, start_response):
214 def __call__(self, environ, start_response):
215 content_path = self.content_path
215 content_path = self.content_path
216 try:
216 try:
217 app = GitRepository(self.repo_name, content_path, self.extras)
217 app = GitRepository(self.repo_name, content_path, self.extras)
218 except (AssertionError, OSError):
218 except (AssertionError, OSError):
219 content_path = os.path.join(content_path, '.git')
219 content_path = os.path.join(content_path, '.git')
220 if os.path.isdir(content_path):
220 if os.path.isdir(content_path):
221 app = GitRepository(self.repo_name, content_path, self.extras)
221 app = GitRepository(self.repo_name, content_path, self.extras)
222 else:
222 else:
223 return exc.HTTPNotFound()(environ, start_response)
223 return exc.HTTPNotFound()(environ, start_response)
224 return app(environ, start_response)
224 return app(environ, start_response)
225
225
226
226
227 def make_wsgi_app(repo_name, repo_root, extras):
227 def make_wsgi_app(repo_name, repo_root, extras):
228 from dulwich.web import LimitedInputFilter, GunzipFilter
228 from dulwich.web import LimitedInputFilter, GunzipFilter
229 app = GitDirectory(repo_root, repo_name, extras)
229 app = GitDirectory(repo_root, repo_name, extras)
230 return GunzipFilter(LimitedInputFilter(app))
230 return GunzipFilter(LimitedInputFilter(app))
@@ -1,648 +1,648 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%text>################################################################################</%text>
2 <%text>################################################################################</%text>
3 <%text>################################################################################</%text>
3 <%text>################################################################################</%text>
4 # Kallithea - config file generated with kallithea-config #
4 # Kallithea - config file generated with kallithea-config #
5 # #
5 # #
6 # The %(here)s variable will be replaced with the parent directory of this file#
6 # The %(here)s variable will be replaced with the parent directory of this file#
7 <%text>################################################################################</%text>
7 <%text>################################################################################</%text>
8 <%text>################################################################################</%text>
8 <%text>################################################################################</%text>
9
9
10 [DEFAULT]
10 [DEFAULT]
11
11
12 <%text>################################################################################</%text>
12 <%text>################################################################################</%text>
13 <%text>## Email settings ##</%text>
13 <%text>## Email settings ##</%text>
14 <%text>## ##</%text>
14 <%text>## ##</%text>
15 <%text>## Refer to the documentation ("Email settings") for more details. ##</%text>
15 <%text>## Refer to the documentation ("Email settings") for more details. ##</%text>
16 <%text>## ##</%text>
16 <%text>## ##</%text>
17 <%text>## It is recommended to use a valid sender address that passes access ##</%text>
17 <%text>## It is recommended to use a valid sender address that passes access ##</%text>
18 <%text>## validation and spam filtering in mail servers. ##</%text>
18 <%text>## validation and spam filtering in mail servers. ##</%text>
19 <%text>################################################################################</%text>
19 <%text>################################################################################</%text>
20
20
21 <%text>## 'From' header for application emails. You can optionally add a name.</%text>
21 <%text>## 'From' header for application emails. You can optionally add a name.</%text>
22 <%text>## Default:</%text>
22 <%text>## Default:</%text>
23 #app_email_from = Kallithea
23 #app_email_from = Kallithea
24 <%text>## Examples:</%text>
24 <%text>## Examples:</%text>
25 #app_email_from = Kallithea <kallithea-noreply@example.com>
25 #app_email_from = Kallithea <kallithea-noreply@example.com>
26 #app_email_from = kallithea-noreply@example.com
26 #app_email_from = kallithea-noreply@example.com
27
27
28 <%text>## Subject prefix for application emails.</%text>
28 <%text>## Subject prefix for application emails.</%text>
29 <%text>## A space between this prefix and the real subject is automatically added.</%text>
29 <%text>## A space between this prefix and the real subject is automatically added.</%text>
30 <%text>## Default:</%text>
30 <%text>## Default:</%text>
31 #email_prefix =
31 #email_prefix =
32 <%text>## Example:</%text>
32 <%text>## Example:</%text>
33 #email_prefix = [Kallithea]
33 #email_prefix = [Kallithea]
34
34
35 <%text>## Recipients for error emails and fallback recipients of application mails.</%text>
35 <%text>## Recipients for error emails and fallback recipients of application mails.</%text>
36 <%text>## Multiple addresses can be specified, comma-separated.</%text>
36 <%text>## Multiple addresses can be specified, comma-separated.</%text>
37 <%text>## Only addresses are allowed, do not add any name part.</%text>
37 <%text>## Only addresses are allowed, do not add any name part.</%text>
38 <%text>## Default:</%text>
38 <%text>## Default:</%text>
39 #email_to =
39 #email_to =
40 <%text>## Examples:</%text>
40 <%text>## Examples:</%text>
41 #email_to = admin@example.com
41 #email_to = admin@example.com
42 #email_to = admin@example.com,another_admin@example.com
42 #email_to = admin@example.com,another_admin@example.com
43 email_to =
43 email_to =
44
44
45 <%text>## 'From' header for error emails. You can optionally add a name.</%text>
45 <%text>## 'From' header for error emails. You can optionally add a name.</%text>
46 <%text>## Default: (none)</%text>
46 <%text>## Default: (none)</%text>
47 <%text>## Examples:</%text>
47 <%text>## Examples:</%text>
48 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
48 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
49 #error_email_from = kallithea_errors@example.com
49 #error_email_from = kallithea_errors@example.com
50 error_email_from =
50 error_email_from =
51
51
52 <%text>## SMTP server settings</%text>
52 <%text>## SMTP server settings</%text>
53 <%text>## If specifying credentials, make sure to use secure connections.</%text>
53 <%text>## If specifying credentials, make sure to use secure connections.</%text>
54 <%text>## Default: Send unencrypted unauthenticated mails to the specified smtp_server.</%text>
54 <%text>## Default: Send unencrypted unauthenticated mails to the specified smtp_server.</%text>
55 <%text>## For "SSL", use smtp_use_ssl = true and smtp_port = 465.</%text>
55 <%text>## For "SSL", use smtp_use_ssl = true and smtp_port = 465.</%text>
56 <%text>## For "STARTTLS", use smtp_use_tls = true and smtp_port = 587.</%text>
56 <%text>## For "STARTTLS", use smtp_use_tls = true and smtp_port = 587.</%text>
57 smtp_server =
57 smtp_server =
58 #smtp_username =
58 #smtp_username =
59 #smtp_password =
59 #smtp_password =
60 smtp_port =
60 smtp_port =
61 #smtp_use_ssl = false
61 #smtp_use_ssl = false
62 #smtp_use_tls = false
62 #smtp_use_tls = false
63
63
64 %if http_server != 'uwsgi':
64 %if http_server != 'uwsgi':
65 <%text>## Entry point for 'gearbox serve'</%text>
65 <%text>## Entry point for 'gearbox serve'</%text>
66 [server:main]
66 [server:main]
67 host = ${host}
67 host = ${host}
68 port = ${port}
68 port = ${port}
69
69
70 %if http_server == 'gearbox':
70 %if http_server == 'gearbox':
71 <%text>## Gearbox default web server ##</%text>
71 <%text>## Gearbox default web server ##</%text>
72 use = egg:gearbox#wsgiref
72 use = egg:gearbox#wsgiref
73 <%text>## nr of worker threads to spawn</%text>
73 <%text>## nr of worker threads to spawn</%text>
74 threadpool_workers = 1
74 threadpool_workers = 1
75 <%text>## max request before thread respawn</%text>
75 <%text>## max request before thread respawn</%text>
76 threadpool_max_requests = 100
76 threadpool_max_requests = 100
77 <%text>## option to use threads of process</%text>
77 <%text>## option to use threads of process</%text>
78 use_threadpool = true
78 use_threadpool = true
79
79
80 %elif http_server == 'gevent':
80 %elif http_server == 'gevent':
81 <%text>## Gearbox gevent web server ##</%text>
81 <%text>## Gearbox gevent web server ##</%text>
82 use = egg:gearbox#gevent
82 use = egg:gearbox#gevent
83
83
84 %elif http_server == 'waitress':
84 %elif http_server == 'waitress':
85 <%text>## WAITRESS ##</%text>
85 <%text>## WAITRESS ##</%text>
86 use = egg:waitress#main
86 use = egg:waitress#main
87 <%text>## number of worker threads</%text>
87 <%text>## number of worker threads</%text>
88 threads = 1
88 threads = 1
89 <%text>## MAX BODY SIZE 100GB</%text>
89 <%text>## MAX BODY SIZE 100GB</%text>
90 max_request_body_size = 107374182400
90 max_request_body_size = 107374182400
91 <%text>## use poll instead of select, fixes fd limits, may not work on old</%text>
91 <%text>## use poll instead of select, fixes fd limits, may not work on old</%text>
92 <%text>## windows systems.</%text>
92 <%text>## windows systems.</%text>
93 #asyncore_use_poll = True
93 #asyncore_use_poll = True
94
94
95 %elif http_server == 'gunicorn':
95 %elif http_server == 'gunicorn':
96 <%text>## GUNICORN ##</%text>
96 <%text>## GUNICORN ##</%text>
97 use = egg:gunicorn#main
97 use = egg:gunicorn#main
98 <%text>## number of process workers. You must set `instance_id = *` when this option</%text>
98 <%text>## number of process workers. You must set `instance_id = *` when this option</%text>
99 <%text>## is set to more than one worker</%text>
99 <%text>## is set to more than one worker</%text>
100 workers = 4
100 workers = 4
101 <%text>## process name</%text>
101 <%text>## process name</%text>
102 proc_name = kallithea
102 proc_name = kallithea
103 <%text>## type of worker class, one of sync, eventlet, gevent, tornado</%text>
103 <%text>## type of worker class, one of sync, eventlet, gevent, tornado</%text>
104 <%text>## recommended for bigger setup is using of of other than sync one</%text>
104 <%text>## recommended for bigger setup is using of of other than sync one</%text>
105 worker_class = sync
105 worker_class = sync
106 max_requests = 1000
106 max_requests = 1000
107 <%text>## amount of time a worker can handle request before it gets killed and</%text>
107 <%text>## amount of time a worker can handle request before it gets killed and</%text>
108 <%text>## restarted</%text>
108 <%text>## restarted</%text>
109 timeout = 3600
109 timeout = 3600
110
110
111 %endif
111 %endif
112 %else:
112 %else:
113 <%text>## UWSGI ##</%text>
113 <%text>## UWSGI ##</%text>
114 <%text>## run with uwsgi --ini-paste-logged <inifile.ini></%text>
114 <%text>## run with uwsgi --ini-paste-logged <inifile.ini></%text>
115 [uwsgi]
115 [uwsgi]
116 socket = /tmp/uwsgi.sock
116 socket = /tmp/uwsgi.sock
117 master = true
117 master = true
118 http = ${host}:${port}
118 http = ${host}:${port}
119
119
120 <%text>## set as daemon and redirect all output to file</%text>
120 <%text>## set as daemon and redirect all output to file</%text>
121 #daemonize = ./uwsgi_kallithea.log
121 #daemonize = ./uwsgi_kallithea.log
122
122
123 <%text>## master process PID</%text>
123 <%text>## master process PID</%text>
124 pidfile = ./uwsgi_kallithea.pid
124 pidfile = ./uwsgi_kallithea.pid
125
125
126 <%text>## stats server with workers statistics, use uwsgitop</%text>
126 <%text>## stats server with workers statistics, use uwsgitop</%text>
127 <%text>## for monitoring, `uwsgitop 127.0.0.1:1717`</%text>
127 <%text>## for monitoring, `uwsgitop 127.0.0.1:1717`</%text>
128 stats = 127.0.0.1:1717
128 stats = 127.0.0.1:1717
129 memory-report = true
129 memory-report = true
130
130
131 <%text>## log 5XX errors</%text>
131 <%text>## log 5XX errors</%text>
132 log-5xx = true
132 log-5xx = true
133
133
134 <%text>## Set the socket listen queue size.</%text>
134 <%text>## Set the socket listen queue size.</%text>
135 listen = 128
135 listen = 128
136
136
137 <%text>## Gracefully Reload workers after the specified amount of managed requests</%text>
137 <%text>## Gracefully Reload workers after the specified amount of managed requests</%text>
138 <%text>## (avoid memory leaks).</%text>
138 <%text>## (avoid memory leaks).</%text>
139 max-requests = 1000
139 max-requests = 1000
140
140
141 <%text>## enable large buffers</%text>
141 <%text>## enable large buffers</%text>
142 buffer-size = 65535
142 buffer-size = 65535
143
143
144 <%text>## socket and http timeouts ##</%text>
144 <%text>## socket and http timeouts ##</%text>
145 http-timeout = 3600
145 http-timeout = 3600
146 socket-timeout = 3600
146 socket-timeout = 3600
147
147
148 <%text>## Log requests slower than the specified number of milliseconds.</%text>
148 <%text>## Log requests slower than the specified number of milliseconds.</%text>
149 log-slow = 10
149 log-slow = 10
150
150
151 <%text>## Exit if no app can be loaded.</%text>
151 <%text>## Exit if no app can be loaded.</%text>
152 need-app = true
152 need-app = true
153
153
154 <%text>## Set lazy mode (load apps in workers instead of master).</%text>
154 <%text>## Set lazy mode (load apps in workers instead of master).</%text>
155 lazy = true
155 lazy = true
156
156
157 <%text>## scaling ##</%text>
157 <%text>## scaling ##</%text>
158 <%text>## set cheaper algorithm to use, if not set default will be used</%text>
158 <%text>## set cheaper algorithm to use, if not set default will be used</%text>
159 cheaper-algo = spare
159 cheaper-algo = spare
160
160
161 <%text>## minimum number of workers to keep at all times</%text>
161 <%text>## minimum number of workers to keep at all times</%text>
162 cheaper = 1
162 cheaper = 1
163
163
164 <%text>## number of workers to spawn at startup</%text>
164 <%text>## number of workers to spawn at startup</%text>
165 cheaper-initial = 1
165 cheaper-initial = 1
166
166
167 <%text>## maximum number of workers that can be spawned</%text>
167 <%text>## maximum number of workers that can be spawned</%text>
168 workers = 4
168 workers = 4
169
169
170 <%text>## how many workers should be spawned at a time</%text>
170 <%text>## how many workers should be spawned at a time</%text>
171 cheaper-step = 1
171 cheaper-step = 1
172
172
173 %endif
173 %endif
174 <%text>## middleware for hosting the WSGI application under a URL prefix</%text>
174 <%text>## middleware for hosting the WSGI application under a URL prefix</%text>
175 #[filter:proxy-prefix]
175 #[filter:proxy-prefix]
176 #use = egg:PasteDeploy#prefix
176 #use = egg:PasteDeploy#prefix
177 #prefix = /<your-prefix>
177 #prefix = /<your-prefix>
178
178
179 [app:main]
179 [app:main]
180 use = egg:kallithea
180 use = egg:kallithea
181 <%text>## enable proxy prefix middleware</%text>
181 <%text>## enable proxy prefix middleware</%text>
182 #filter-with = proxy-prefix
182 #filter-with = proxy-prefix
183
183
184 full_stack = true
184 full_stack = true
185 static_files = true
185 static_files = true
186
186
187 <%text>## Internationalization (see setup documentation for details)</%text>
187 <%text>## Internationalization (see setup documentation for details)</%text>
188 <%text>## By default, the language requested by the browser is used if available.</%text>
188 <%text>## By default, the language requested by the browser is used if available.</%text>
189 #i18n.enable = false
189 #i18n.enable = false
190 <%text>## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):</%text>
190 <%text>## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):</%text>
191 i18n.lang =
191 i18n.lang =
192
192
193 cache_dir = %(here)s/data
193 cache_dir = %(here)s/data
194 index_dir = %(here)s/data/index
194 index_dir = %(here)s/data/index
195
195
196 <%text>## uncomment and set this path to use archive download cache</%text>
196 <%text>## uncomment and set this path to use archive download cache</%text>
197 archive_cache_dir = %(here)s/tarballcache
197 archive_cache_dir = %(here)s/tarballcache
198
198
199 <%text>## change this to unique ID for security</%text>
199 <%text>## change this to unique ID for security</%text>
200 app_instance_uuid = ${uuid()}
200 app_instance_uuid = ${uuid()}
201
201
202 <%text>## cut off limit for large diffs (size in bytes)</%text>
202 <%text>## cut off limit for large diffs (size in bytes)</%text>
203 cut_off_limit = 256000
203 cut_off_limit = 256000
204
204
205 <%text>## force https in Kallithea, fixes https redirects, assumes it's always https</%text>
205 <%text>## force https in Kallithea, fixes https redirects, assumes it's always https</%text>
206 force_https = false
206 force_https = false
207
207
208 <%text>## use Strict-Transport-Security headers</%text>
208 <%text>## use Strict-Transport-Security headers</%text>
209 use_htsts = false
209 use_htsts = false
210
210
211 <%text>## number of commits stats will parse on each iteration</%text>
211 <%text>## number of commits stats will parse on each iteration</%text>
212 commit_parse_limit = 25
212 commit_parse_limit = 25
213
213
214 <%text>## path to git executable</%text>
214 <%text>## path to git executable</%text>
215 git_path = git
215 git_path = git
216
216
217 <%text>## git rev filter option, --all is the default filter, if you need to</%text>
217 <%text>## git rev filter option, --all is the default filter, if you need to</%text>
218 <%text>## hide all refs in changelog switch this to --branches --tags</%text>
218 <%text>## hide all refs in changelog switch this to --branches --tags</%text>
219 #git_rev_filter = --branches --tags
219 #git_rev_filter = --branches --tags
220
220
221 <%text>## RSS feed options</%text>
221 <%text>## RSS feed options</%text>
222 rss_cut_off_limit = 256000
222 rss_cut_off_limit = 256000
223 rss_items_per_page = 10
223 rss_items_per_page = 10
224 rss_include_diff = false
224 rss_include_diff = false
225
225
226 <%text>## options for showing and identifying changesets</%text>
226 <%text>## options for showing and identifying changesets</%text>
227 show_sha_length = 12
227 show_sha_length = 12
228 show_revision_number = false
228 show_revision_number = false
229
229
230 <%text>## Canonical URL to use when creating full URLs in UI and texts.</%text>
230 <%text>## Canonical URL to use when creating full URLs in UI and texts.</%text>
231 <%text>## Useful when the site is available under different names or protocols.</%text>
231 <%text>## Useful when the site is available under different names or protocols.</%text>
232 <%text>## Defaults to what is provided in the WSGI environment.</%text>
232 <%text>## Defaults to what is provided in the WSGI environment.</%text>
233 #canonical_url = https://kallithea.example.com/repos
233 #canonical_url = https://kallithea.example.com/repos
234
234
235 <%text>## gist URL alias, used to create nicer urls for gist. This should be an</%text>
235 <%text>## gist URL alias, used to create nicer urls for gist. This should be an</%text>
236 <%text>## url that does rewrites to _admin/gists/<gistid>.</%text>
236 <%text>## url that does rewrites to _admin/gists/<gistid>.</%text>
237 <%text>## example: http://gist.example.com/{gistid}. Empty means use the internal</%text>
237 <%text>## example: http://gist.example.com/{gistid}. Empty means use the internal</%text>
238 <%text>## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid></%text>
238 <%text>## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid></%text>
239 gist_alias_url =
239 gist_alias_url =
240
240
241 <%text>## white list of API enabled controllers. This allows to add list of</%text>
241 <%text>## white list of API enabled controllers. This allows to add list of</%text>
242 <%text>## controllers to which access will be enabled by api_key. eg: to enable</%text>
242 <%text>## controllers to which access will be enabled by api_key. eg: to enable</%text>
243 <%text>## api access to raw_files put `FilesController:raw`, to enable access to patches</%text>
243 <%text>## api access to raw_files put `FilesController:raw`, to enable access to patches</%text>
244 <%text>## add `ChangesetController:changeset_patch`. This list should be "," separated</%text>
244 <%text>## add `ChangesetController:changeset_patch`. This list should be "," separated</%text>
245 <%text>## Syntax is <ControllerClass>:<function>. Check debug logs for generated names</%text>
245 <%text>## Syntax is <ControllerClass>:<function>. Check debug logs for generated names</%text>
246 <%text>## Recommended settings below are commented out:</%text>
246 <%text>## Recommended settings below are commented out:</%text>
247 api_access_controllers_whitelist =
247 api_access_controllers_whitelist =
248 # ChangesetController:changeset_patch,
248 # ChangesetController:changeset_patch,
249 # ChangesetController:changeset_raw,
249 # ChangesetController:changeset_raw,
250 # FilesController:raw,
250 # FilesController:raw,
251 # FilesController:archivefile
251 # FilesController:archivefile
252
252
253 <%text>## default encoding used to convert from and to unicode</%text>
253 <%text>## default encoding used to convert from and to unicode</%text>
254 <%text>## can be also a comma separated list of encoding in case of mixed encodings</%text>
254 <%text>## can be also a comma separated list of encoding in case of mixed encodings</%text>
255 default_encoding = utf8
255 default_encoding = utf-8
256
256
257 <%text>## Set Mercurial encoding, similar to setting HGENCODING before launching Kallithea</%text>
257 <%text>## Set Mercurial encoding, similar to setting HGENCODING before launching Kallithea</%text>
258 hgencoding = utf-8
258 hgencoding = utf-8
259
259
260 <%text>## issue tracker for Kallithea (leave blank to disable, absent for default)</%text>
260 <%text>## issue tracker for Kallithea (leave blank to disable, absent for default)</%text>
261 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
261 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
262
262
263 <%text>## issue tracking mapping for commit messages, comments, PR descriptions, ...</%text>
263 <%text>## issue tracking mapping for commit messages, comments, PR descriptions, ...</%text>
264 <%text>## Refer to the documentation ("Integration with issue trackers") for more details.</%text>
264 <%text>## Refer to the documentation ("Integration with issue trackers") for more details.</%text>
265
265
266 <%text>## regular expression to match issue references</%text>
266 <%text>## regular expression to match issue references</%text>
267 <%text>## This pattern may/should contain parenthesized groups, that can</%text>
267 <%text>## This pattern may/should contain parenthesized groups, that can</%text>
268 <%text>## be referred to in issue_server_link or issue_sub using Python backreferences</%text>
268 <%text>## be referred to in issue_server_link or issue_sub using Python backreferences</%text>
269 <%text>## (e.g. \1, \2, ...). You can also create named groups with '(?P<groupname>)'.</%text>
269 <%text>## (e.g. \1, \2, ...). You can also create named groups with '(?P<groupname>)'.</%text>
270 <%text>## To require mandatory whitespace before the issue pattern, use:</%text>
270 <%text>## To require mandatory whitespace before the issue pattern, use:</%text>
271 <%text>## (?:^|(?<=\s)) before the actual pattern, and for mandatory whitespace</%text>
271 <%text>## (?:^|(?<=\s)) before the actual pattern, and for mandatory whitespace</%text>
272 <%text>## behind the issue pattern, use (?:$|(?=\s)) after the actual pattern.</%text>
272 <%text>## behind the issue pattern, use (?:$|(?=\s)) after the actual pattern.</%text>
273
273
274 issue_pat = #(\d+)
274 issue_pat = #(\d+)
275
275
276 <%text>## server url to the issue</%text>
276 <%text>## server url to the issue</%text>
277 <%text>## This pattern may/should contain backreferences to parenthesized groups in issue_pat.</%text>
277 <%text>## This pattern may/should contain backreferences to parenthesized groups in issue_pat.</%text>
278 <%text>## A backreference can be \1, \2, ... or \g<groupname> if you specified a named group</%text>
278 <%text>## A backreference can be \1, \2, ... or \g<groupname> if you specified a named group</%text>
279 <%text>## called 'groupname' in issue_pat.</%text>
279 <%text>## called 'groupname' in issue_pat.</%text>
280 <%text>## The special token {repo} is replaced with the full repository name</%text>
280 <%text>## The special token {repo} is replaced with the full repository name</%text>
281 <%text>## including repository groups, while {repo_name} is replaced with just</%text>
281 <%text>## including repository groups, while {repo_name} is replaced with just</%text>
282 <%text>## the name of the repository.</%text>
282 <%text>## the name of the repository.</%text>
283
283
284 issue_server_link = https://issues.example.com/{repo}/issue/\1
284 issue_server_link = https://issues.example.com/{repo}/issue/\1
285
285
286 <%text>## substitution pattern to use as the link text</%text>
286 <%text>## substitution pattern to use as the link text</%text>
287 <%text>## If issue_sub is empty, the text matched by issue_pat is retained verbatim</%text>
287 <%text>## If issue_sub is empty, the text matched by issue_pat is retained verbatim</%text>
288 <%text>## for the link text. Otherwise, the link text is that of issue_sub, with any</%text>
288 <%text>## for the link text. Otherwise, the link text is that of issue_sub, with any</%text>
289 <%text>## backreferences to groups in issue_pat replaced.</%text>
289 <%text>## backreferences to groups in issue_pat replaced.</%text>
290
290
291 issue_sub =
291 issue_sub =
292
292
293 <%text>## issue_pat, issue_server_link and issue_sub can have suffixes to specify</%text>
293 <%text>## issue_pat, issue_server_link and issue_sub can have suffixes to specify</%text>
294 <%text>## multiple patterns, to other issues server, wiki or others</%text>
294 <%text>## multiple patterns, to other issues server, wiki or others</%text>
295 <%text>## below an example how to create a wiki pattern</%text>
295 <%text>## below an example how to create a wiki pattern</%text>
296 # wiki-some-id -> https://wiki.example.com/some-id
296 # wiki-some-id -> https://wiki.example.com/some-id
297
297
298 #issue_pat_wiki = wiki-(\S+)
298 #issue_pat_wiki = wiki-(\S+)
299 #issue_server_link_wiki = https://wiki.example.com/\1
299 #issue_server_link_wiki = https://wiki.example.com/\1
300 #issue_sub_wiki = WIKI-\1
300 #issue_sub_wiki = WIKI-\1
301
301
302 <%text>## alternative return HTTP header for failed authentication. Default HTTP</%text>
302 <%text>## alternative return HTTP header for failed authentication. Default HTTP</%text>
303 <%text>## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with</%text>
303 <%text>## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with</%text>
304 <%text>## handling that. Set this variable to 403 to return HTTPForbidden</%text>
304 <%text>## handling that. Set this variable to 403 to return HTTPForbidden</%text>
305 auth_ret_code =
305 auth_ret_code =
306
306
307 <%text>## locking return code. When repository is locked return this HTTP code. 2XX</%text>
307 <%text>## locking return code. When repository is locked return this HTTP code. 2XX</%text>
308 <%text>## codes don't break the transactions while 4XX codes do</%text>
308 <%text>## codes don't break the transactions while 4XX codes do</%text>
309 lock_ret_code = 423
309 lock_ret_code = 423
310
310
311 <%text>## allows to change the repository location in settings page</%text>
311 <%text>## allows to change the repository location in settings page</%text>
312 allow_repo_location_change = True
312 allow_repo_location_change = True
313
313
314 <%text>## allows to setup custom hooks in settings page</%text>
314 <%text>## allows to setup custom hooks in settings page</%text>
315 allow_custom_hooks_settings = True
315 allow_custom_hooks_settings = True
316
316
317 <%text>## extra extensions for indexing, space separated and without the leading '.'.</%text>
317 <%text>## extra extensions for indexing, space separated and without the leading '.'.</%text>
318 # index.extensions =
318 # index.extensions =
319 # gemfile
319 # gemfile
320 # lock
320 # lock
321
321
322 <%text>## extra filenames for indexing, space separated</%text>
322 <%text>## extra filenames for indexing, space separated</%text>
323 # index.filenames =
323 # index.filenames =
324 # .dockerignore
324 # .dockerignore
325 # .editorconfig
325 # .editorconfig
326 # INSTALL
326 # INSTALL
327 # CHANGELOG
327 # CHANGELOG
328
328
329 <%text>####################################</%text>
329 <%text>####################################</%text>
330 <%text>### CELERY CONFIG ####</%text>
330 <%text>### CELERY CONFIG ####</%text>
331 <%text>####################################</%text>
331 <%text>####################################</%text>
332
332
333 use_celery = false
333 use_celery = false
334
334
335 <%text>## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:</%text>
335 <%text>## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:</%text>
336 broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
336 broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
337
337
338 celery.imports = kallithea.lib.celerylib.tasks
338 celery.imports = kallithea.lib.celerylib.tasks
339 celery.accept.content = pickle
339 celery.accept.content = pickle
340 celery.result.backend = amqp
340 celery.result.backend = amqp
341 celery.result.dburi = amqp://
341 celery.result.dburi = amqp://
342 celery.result.serialier = json
342 celery.result.serialier = json
343
343
344 #celery.send.task.error.emails = true
344 #celery.send.task.error.emails = true
345 #celery.amqp.task.result.expires = 18000
345 #celery.amqp.task.result.expires = 18000
346
346
347 celeryd.concurrency = 2
347 celeryd.concurrency = 2
348 celeryd.max.tasks.per.child = 1
348 celeryd.max.tasks.per.child = 1
349
349
350 <%text>## If true, tasks will never be sent to the queue, but executed locally instead.</%text>
350 <%text>## If true, tasks will never be sent to the queue, but executed locally instead.</%text>
351 celery.always.eager = false
351 celery.always.eager = false
352
352
353 <%text>####################################</%text>
353 <%text>####################################</%text>
354 <%text>### BEAKER CACHE ####</%text>
354 <%text>### BEAKER CACHE ####</%text>
355 <%text>####################################</%text>
355 <%text>####################################</%text>
356
356
357 beaker.cache.data_dir = %(here)s/data/cache/data
357 beaker.cache.data_dir = %(here)s/data/cache/data
358 beaker.cache.lock_dir = %(here)s/data/cache/lock
358 beaker.cache.lock_dir = %(here)s/data/cache/lock
359
359
360 beaker.cache.regions = short_term,long_term,sql_cache_short
360 beaker.cache.regions = short_term,long_term,sql_cache_short
361
361
362 beaker.cache.short_term.type = memory
362 beaker.cache.short_term.type = memory
363 beaker.cache.short_term.expire = 60
363 beaker.cache.short_term.expire = 60
364 beaker.cache.short_term.key_length = 256
364 beaker.cache.short_term.key_length = 256
365
365
366 beaker.cache.long_term.type = memory
366 beaker.cache.long_term.type = memory
367 beaker.cache.long_term.expire = 36000
367 beaker.cache.long_term.expire = 36000
368 beaker.cache.long_term.key_length = 256
368 beaker.cache.long_term.key_length = 256
369
369
370 beaker.cache.sql_cache_short.type = memory
370 beaker.cache.sql_cache_short.type = memory
371 beaker.cache.sql_cache_short.expire = 10
371 beaker.cache.sql_cache_short.expire = 10
372 beaker.cache.sql_cache_short.key_length = 256
372 beaker.cache.sql_cache_short.key_length = 256
373
373
374 <%text>####################################</%text>
374 <%text>####################################</%text>
375 <%text>### BEAKER SESSION ####</%text>
375 <%text>### BEAKER SESSION ####</%text>
376 <%text>####################################</%text>
376 <%text>####################################</%text>
377
377
378 <%text>## Name of session cookie. Should be unique for a given host and path, even when running</%text>
378 <%text>## Name of session cookie. Should be unique for a given host and path, even when running</%text>
379 <%text>## on different ports. Otherwise, cookie sessions will be shared and messed up.</%text>
379 <%text>## on different ports. Otherwise, cookie sessions will be shared and messed up.</%text>
380 beaker.session.key = kallithea
380 beaker.session.key = kallithea
381 <%text>## Sessions should always only be accessible by the browser, not directly by JavaScript.</%text>
381 <%text>## Sessions should always only be accessible by the browser, not directly by JavaScript.</%text>
382 beaker.session.httponly = true
382 beaker.session.httponly = true
383 <%text>## Session lifetime. 2592000 seconds is 30 days.</%text>
383 <%text>## Session lifetime. 2592000 seconds is 30 days.</%text>
384 beaker.session.timeout = 2592000
384 beaker.session.timeout = 2592000
385
385
386 <%text>## Server secret used with HMAC to ensure integrity of cookies.</%text>
386 <%text>## Server secret used with HMAC to ensure integrity of cookies.</%text>
387 beaker.session.secret = ${uuid()}
387 beaker.session.secret = ${uuid()}
388 <%text>## Further, encrypt the data with AES.</%text>
388 <%text>## Further, encrypt the data with AES.</%text>
389 #beaker.session.encrypt_key = <key_for_encryption>
389 #beaker.session.encrypt_key = <key_for_encryption>
390 #beaker.session.validate_key = <validation_key>
390 #beaker.session.validate_key = <validation_key>
391
391
392 <%text>## Type of storage used for the session, current types are</%text>
392 <%text>## Type of storage used for the session, current types are</%text>
393 <%text>## dbm, file, memcached, database, and memory.</%text>
393 <%text>## dbm, file, memcached, database, and memory.</%text>
394
394
395 <%text>## File system storage of session data. (default)</%text>
395 <%text>## File system storage of session data. (default)</%text>
396 #beaker.session.type = file
396 #beaker.session.type = file
397
397
398 <%text>## Cookie only, store all session data inside the cookie. Requires secure secrets.</%text>
398 <%text>## Cookie only, store all session data inside the cookie. Requires secure secrets.</%text>
399 #beaker.session.type = cookie
399 #beaker.session.type = cookie
400
400
401 <%text>## Database storage of session data.</%text>
401 <%text>## Database storage of session data.</%text>
402 #beaker.session.type = ext:database
402 #beaker.session.type = ext:database
403 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
403 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
404 #beaker.session.table_name = db_session
404 #beaker.session.table_name = db_session
405
405
406 %if error_aggregation_service == 'appenlight':
406 %if error_aggregation_service == 'appenlight':
407 <%text>############################</%text>
407 <%text>############################</%text>
408 <%text>## ERROR HANDLING SYSTEMS ##</%text>
408 <%text>## ERROR HANDLING SYSTEMS ##</%text>
409 <%text>############################</%text>
409 <%text>############################</%text>
410
410
411 # Propagate email settings to ErrorReporter of TurboGears2
411 # Propagate email settings to ErrorReporter of TurboGears2
412 # You do not normally need to change these lines
412 # You do not normally need to change these lines
413 get trace_errors.error_email = email_to
413 get trace_errors.error_email = email_to
414 get trace_errors.smtp_server = smtp_server
414 get trace_errors.smtp_server = smtp_server
415 get trace_errors.smtp_port = smtp_port
415 get trace_errors.smtp_port = smtp_port
416 get trace_errors.from_address = error_email_from
416 get trace_errors.from_address = error_email_from
417
417
418 <%text>####################</%text>
418 <%text>####################</%text>
419 <%text>### [appenlight] ###</%text>
419 <%text>### [appenlight] ###</%text>
420 <%text>####################</%text>
420 <%text>####################</%text>
421
421
422 <%text>## AppEnlight is tailored to work with Kallithea, see</%text>
422 <%text>## AppEnlight is tailored to work with Kallithea, see</%text>
423 <%text>## http://appenlight.com for details how to obtain an account</%text>
423 <%text>## http://appenlight.com for details how to obtain an account</%text>
424 <%text>## you must install python package `appenlight_client` to make it work</%text>
424 <%text>## you must install python package `appenlight_client` to make it work</%text>
425
425
426 <%text>## appenlight enabled</%text>
426 <%text>## appenlight enabled</%text>
427 appenlight = false
427 appenlight = false
428
428
429 appenlight.server_url = https://api.appenlight.com
429 appenlight.server_url = https://api.appenlight.com
430 appenlight.api_key = YOUR_API_KEY
430 appenlight.api_key = YOUR_API_KEY
431
431
432 <%text>## TWEAK AMOUNT OF INFO SENT HERE</%text>
432 <%text>## TWEAK AMOUNT OF INFO SENT HERE</%text>
433
433
434 <%text>## enables 404 error logging (default False)</%text>
434 <%text>## enables 404 error logging (default False)</%text>
435 appenlight.report_404 = false
435 appenlight.report_404 = false
436
436
437 <%text>## time in seconds after request is considered being slow (default 1)</%text>
437 <%text>## time in seconds after request is considered being slow (default 1)</%text>
438 appenlight.slow_request_time = 1
438 appenlight.slow_request_time = 1
439
439
440 <%text>## record slow requests in application</%text>
440 <%text>## record slow requests in application</%text>
441 <%text>## (needs to be enabled for slow datastore recording and time tracking)</%text>
441 <%text>## (needs to be enabled for slow datastore recording and time tracking)</%text>
442 appenlight.slow_requests = true
442 appenlight.slow_requests = true
443
443
444 <%text>## enable hooking to application loggers</%text>
444 <%text>## enable hooking to application loggers</%text>
445 #appenlight.logging = true
445 #appenlight.logging = true
446
446
447 <%text>## minimum log level for log capture</%text>
447 <%text>## minimum log level for log capture</%text>
448 #appenlight.logging.level = WARNING
448 #appenlight.logging.level = WARNING
449
449
450 <%text>## send logs only from erroneous/slow requests</%text>
450 <%text>## send logs only from erroneous/slow requests</%text>
451 <%text>## (saves API quota for intensive logging)</%text>
451 <%text>## (saves API quota for intensive logging)</%text>
452 appenlight.logging_on_error = false
452 appenlight.logging_on_error = false
453
453
454 <%text>## list of additional keywords that should be grabbed from environ object</%text>
454 <%text>## list of additional keywords that should be grabbed from environ object</%text>
455 <%text>## can be string with comma separated list of words in lowercase</%text>
455 <%text>## can be string with comma separated list of words in lowercase</%text>
456 <%text>## (by default client will always send following info:</%text>
456 <%text>## (by default client will always send following info:</%text>
457 <%text>## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that</%text>
457 <%text>## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that</%text>
458 <%text>## start with HTTP* this list be extended with additional keywords here</%text>
458 <%text>## start with HTTP* this list be extended with additional keywords here</%text>
459 appenlight.environ_keys_whitelist =
459 appenlight.environ_keys_whitelist =
460
460
461 <%text>## list of keywords that should be blanked from request object</%text>
461 <%text>## list of keywords that should be blanked from request object</%text>
462 <%text>## can be string with comma separated list of words in lowercase</%text>
462 <%text>## can be string with comma separated list of words in lowercase</%text>
463 <%text>## (by default client will always blank keys that contain following words</%text>
463 <%text>## (by default client will always blank keys that contain following words</%text>
464 <%text>## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'</%text>
464 <%text>## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'</%text>
465 <%text>## this list be extended with additional keywords set here</%text>
465 <%text>## this list be extended with additional keywords set here</%text>
466 appenlight.request_keys_blacklist =
466 appenlight.request_keys_blacklist =
467
467
468 <%text>## list of namespaces that should be ignores when gathering log entries</%text>
468 <%text>## list of namespaces that should be ignores when gathering log entries</%text>
469 <%text>## can be string with comma separated list of namespaces</%text>
469 <%text>## can be string with comma separated list of namespaces</%text>
470 <%text>## (by default the client ignores own entries: appenlight_client.client)</%text>
470 <%text>## (by default the client ignores own entries: appenlight_client.client)</%text>
471 appenlight.log_namespace_blacklist =
471 appenlight.log_namespace_blacklist =
472
472
473 %elif error_aggregation_service == 'sentry':
473 %elif error_aggregation_service == 'sentry':
474 <%text>################</%text>
474 <%text>################</%text>
475 <%text>### [sentry] ###</%text>
475 <%text>### [sentry] ###</%text>
476 <%text>################</%text>
476 <%text>################</%text>
477
477
478 <%text>## sentry is a alternative open source error aggregator</%text>
478 <%text>## sentry is a alternative open source error aggregator</%text>
479 <%text>## you must install python packages `sentry` and `raven` to enable</%text>
479 <%text>## you must install python packages `sentry` and `raven` to enable</%text>
480
480
481 sentry.dsn = YOUR_DNS
481 sentry.dsn = YOUR_DNS
482 sentry.servers =
482 sentry.servers =
483 sentry.name =
483 sentry.name =
484 sentry.key =
484 sentry.key =
485 sentry.public_key =
485 sentry.public_key =
486 sentry.secret_key =
486 sentry.secret_key =
487 sentry.project =
487 sentry.project =
488 sentry.site =
488 sentry.site =
489 sentry.include_paths =
489 sentry.include_paths =
490 sentry.exclude_paths =
490 sentry.exclude_paths =
491
491
492 %endif
492 %endif
493 <%text>################################################################################</%text>
493 <%text>################################################################################</%text>
494 <%text>## WARNING: *DEBUG MODE MUST BE OFF IN A PRODUCTION ENVIRONMENT* ##</%text>
494 <%text>## WARNING: *DEBUG MODE MUST BE OFF IN A PRODUCTION ENVIRONMENT* ##</%text>
495 <%text>## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##</%text>
495 <%text>## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##</%text>
496 <%text>## execute malicious code after an exception is raised. ##</%text>
496 <%text>## execute malicious code after an exception is raised. ##</%text>
497 <%text>################################################################################</%text>
497 <%text>################################################################################</%text>
498 debug = false
498 debug = false
499
499
500 <%text>##################################</%text>
500 <%text>##################################</%text>
501 <%text>### LOGVIEW CONFIG ###</%text>
501 <%text>### LOGVIEW CONFIG ###</%text>
502 <%text>##################################</%text>
502 <%text>##################################</%text>
503
503
504 logview.sqlalchemy = #faa
504 logview.sqlalchemy = #faa
505 logview.pylons.templating = #bfb
505 logview.pylons.templating = #bfb
506 logview.pylons.util = #eee
506 logview.pylons.util = #eee
507
507
508 <%text>#########################################################</%text>
508 <%text>#########################################################</%text>
509 <%text>### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###</%text>
509 <%text>### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###</%text>
510 <%text>#########################################################</%text>
510 <%text>#########################################################</%text>
511
511
512 %if database_engine == 'sqlite':
512 %if database_engine == 'sqlite':
513 # SQLITE [default]
513 # SQLITE [default]
514 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
514 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
515
515
516 %elif database_engine == 'postgres':
516 %elif database_engine == 'postgres':
517 # POSTGRESQL
517 # POSTGRESQL
518 sqlalchemy.url = postgresql://user:pass@localhost/kallithea
518 sqlalchemy.url = postgresql://user:pass@localhost/kallithea
519
519
520 %elif database_engine == 'mysql':
520 %elif database_engine == 'mysql':
521 # MySQL
521 # MySQL
522 sqlalchemy.url = mysql://user:pass@localhost/kallithea?charset=utf8
522 sqlalchemy.url = mysql://user:pass@localhost/kallithea?charset=utf8
523
523
524 %endif
524 %endif
525 # see sqlalchemy docs for others
525 # see sqlalchemy docs for others
526
526
527 sqlalchemy.pool_recycle = 3600
527 sqlalchemy.pool_recycle = 3600
528
528
529 <%text>################################</%text>
529 <%text>################################</%text>
530 <%text>### ALEMBIC CONFIGURATION ####</%text>
530 <%text>### ALEMBIC CONFIGURATION ####</%text>
531 <%text>################################</%text>
531 <%text>################################</%text>
532
532
533 [alembic]
533 [alembic]
534 script_location = kallithea:alembic
534 script_location = kallithea:alembic
535
535
536 <%text>################################</%text>
536 <%text>################################</%text>
537 <%text>### LOGGING CONFIGURATION ####</%text>
537 <%text>### LOGGING CONFIGURATION ####</%text>
538 <%text>################################</%text>
538 <%text>################################</%text>
539
539
540 [loggers]
540 [loggers]
541 keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer, werkzeug, backlash
541 keys = root, routes, kallithea, sqlalchemy, tg, gearbox, beaker, templates, whoosh_indexer, werkzeug, backlash
542
542
543 [handlers]
543 [handlers]
544 keys = console, console_sql
544 keys = console, console_sql
545
545
546 [formatters]
546 [formatters]
547 keys = generic, color_formatter, color_formatter_sql
547 keys = generic, color_formatter, color_formatter_sql
548
548
549 <%text>#############</%text>
549 <%text>#############</%text>
550 <%text>## LOGGERS ##</%text>
550 <%text>## LOGGERS ##</%text>
551 <%text>#############</%text>
551 <%text>#############</%text>
552
552
553 [logger_root]
553 [logger_root]
554 level = NOTSET
554 level = NOTSET
555 handlers = console
555 handlers = console
556
556
557 [logger_routes]
557 [logger_routes]
558 level = WARN
558 level = WARN
559 handlers =
559 handlers =
560 qualname = routes.middleware
560 qualname = routes.middleware
561 <%text>## "level = DEBUG" logs the route matched and routing variables.</%text>
561 <%text>## "level = DEBUG" logs the route matched and routing variables.</%text>
562 propagate = 1
562 propagate = 1
563
563
564 [logger_beaker]
564 [logger_beaker]
565 level = WARN
565 level = WARN
566 handlers =
566 handlers =
567 qualname = beaker.container
567 qualname = beaker.container
568 propagate = 1
568 propagate = 1
569
569
570 [logger_templates]
570 [logger_templates]
571 level = WARN
571 level = WARN
572 handlers =
572 handlers =
573 qualname = pylons.templating
573 qualname = pylons.templating
574 propagate = 1
574 propagate = 1
575
575
576 [logger_kallithea]
576 [logger_kallithea]
577 level = WARN
577 level = WARN
578 handlers =
578 handlers =
579 qualname = kallithea
579 qualname = kallithea
580 propagate = 1
580 propagate = 1
581
581
582 [logger_tg]
582 [logger_tg]
583 level = WARN
583 level = WARN
584 handlers =
584 handlers =
585 qualname = tg
585 qualname = tg
586 propagate = 1
586 propagate = 1
587
587
588 [logger_gearbox]
588 [logger_gearbox]
589 level = WARN
589 level = WARN
590 handlers =
590 handlers =
591 qualname = gearbox
591 qualname = gearbox
592 propagate = 1
592 propagate = 1
593
593
594 [logger_sqlalchemy]
594 [logger_sqlalchemy]
595 level = WARN
595 level = WARN
596 handlers = console_sql
596 handlers = console_sql
597 qualname = sqlalchemy.engine
597 qualname = sqlalchemy.engine
598 propagate = 0
598 propagate = 0
599
599
600 [logger_whoosh_indexer]
600 [logger_whoosh_indexer]
601 level = WARN
601 level = WARN
602 handlers =
602 handlers =
603 qualname = whoosh_indexer
603 qualname = whoosh_indexer
604 propagate = 1
604 propagate = 1
605
605
606 [logger_werkzeug]
606 [logger_werkzeug]
607 level = WARN
607 level = WARN
608 handlers =
608 handlers =
609 qualname = werkzeug
609 qualname = werkzeug
610 propagate = 1
610 propagate = 1
611
611
612 [logger_backlash]
612 [logger_backlash]
613 level = WARN
613 level = WARN
614 handlers =
614 handlers =
615 qualname = backlash
615 qualname = backlash
616 propagate = 1
616 propagate = 1
617
617
618 <%text>##############</%text>
618 <%text>##############</%text>
619 <%text>## HANDLERS ##</%text>
619 <%text>## HANDLERS ##</%text>
620 <%text>##############</%text>
620 <%text>##############</%text>
621
621
622 [handler_console]
622 [handler_console]
623 class = StreamHandler
623 class = StreamHandler
624 args = (sys.stderr,)
624 args = (sys.stderr,)
625 formatter = generic
625 formatter = generic
626
626
627 [handler_console_sql]
627 [handler_console_sql]
628 class = StreamHandler
628 class = StreamHandler
629 args = (sys.stderr,)
629 args = (sys.stderr,)
630 formatter = generic
630 formatter = generic
631
631
632 <%text>################</%text>
632 <%text>################</%text>
633 <%text>## FORMATTERS ##</%text>
633 <%text>## FORMATTERS ##</%text>
634 <%text>################</%text>
634 <%text>################</%text>
635
635
636 [formatter_generic]
636 [formatter_generic]
637 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
637 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
638 datefmt = %Y-%m-%d %H:%M:%S
638 datefmt = %Y-%m-%d %H:%M:%S
639
639
640 [formatter_color_formatter]
640 [formatter_color_formatter]
641 class = kallithea.lib.colored_formatter.ColorFormatter
641 class = kallithea.lib.colored_formatter.ColorFormatter
642 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
642 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
643 datefmt = %Y-%m-%d %H:%M:%S
643 datefmt = %Y-%m-%d %H:%M:%S
644
644
645 [formatter_color_formatter_sql]
645 [formatter_color_formatter_sql]
646 class = kallithea.lib.colored_formatter.ColorFormatterSql
646 class = kallithea.lib.colored_formatter.ColorFormatterSql
647 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
647 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
648 datefmt = %Y-%m-%d %H:%M:%S
648 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,710 +1,710 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils
15 kallithea.lib.utils
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Utilities library for Kallithea
18 Utilities library for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 18, 2010
22 :created_on: Apr 18, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import os
28 import os
29 import re
29 import re
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import traceback
32 import traceback
33 import beaker
33 import beaker
34
34
35 from tg import request, response
35 from tg import request, response
36 from tg.i18n import ugettext as _
36 from tg.i18n import ugettext as _
37 from webhelpers.text import collapse, remove_formatting, strip_tags
37 from webhelpers.text import collapse, remove_formatting, strip_tags
38 from beaker.cache import _cache_decorate
38 from beaker.cache import _cache_decorate
39
39
40 from kallithea.lib.vcs.utils.hgcompat import ui, config
40 from kallithea.lib.vcs.utils.hgcompat import ui, config
41 from kallithea.lib.vcs.utils.helpers import get_scm
41 from kallithea.lib.vcs.utils.helpers import get_scm
42 from kallithea.lib.vcs.exceptions import VCSError
42 from kallithea.lib.vcs.exceptions import VCSError
43
43
44 from kallithea.lib.exceptions import HgsubversionImportError
44 from kallithea.lib.exceptions import HgsubversionImportError
45 from kallithea.model import meta
45 from kallithea.model import meta
46 from kallithea.model.db import Repository, User, Ui, \
46 from kallithea.model.db import Repository, User, Ui, \
47 UserLog, RepoGroup, Setting, UserGroup
47 UserLog, RepoGroup, Setting, UserGroup
48 from kallithea.model.repo_group import RepoGroupModel
48 from kallithea.model.repo_group import RepoGroupModel
49 from kallithea.lib.utils2 import safe_str, safe_unicode, get_current_authuser
49 from kallithea.lib.utils2 import safe_str, safe_unicode, get_current_authuser
50 from kallithea.lib.vcs.utils.fakemod import create_module
50 from kallithea.lib.vcs.utils.fakemod import create_module
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
54 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
55
55
56
56
57 def recursive_replace(str_, replace=' '):
57 def recursive_replace(str_, replace=' '):
58 """
58 """
59 Recursive replace of given sign to just one instance
59 Recursive replace of given sign to just one instance
60
60
61 :param str_: given string
61 :param str_: given string
62 :param replace: char to find and replace multiple instances
62 :param replace: char to find and replace multiple instances
63
63
64 Examples::
64 Examples::
65 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
65 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
66 'Mighty-Mighty-Bo-sstones'
66 'Mighty-Mighty-Bo-sstones'
67 """
67 """
68
68
69 if str_.find(replace * 2) == -1:
69 if str_.find(replace * 2) == -1:
70 return str_
70 return str_
71 else:
71 else:
72 str_ = str_.replace(replace * 2, replace)
72 str_ = str_.replace(replace * 2, replace)
73 return recursive_replace(str_, replace)
73 return recursive_replace(str_, replace)
74
74
75
75
76 def repo_name_slug(value):
76 def repo_name_slug(value):
77 """
77 """
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 #==============================================================================
93 #==============================================================================
94 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
94 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
95 #==============================================================================
95 #==============================================================================
96 def get_repo_slug(request):
96 def get_repo_slug(request):
97 _repo = request.environ['pylons.routes_dict'].get('repo_name')
97 _repo = request.environ['pylons.routes_dict'].get('repo_name')
98 if _repo:
98 if _repo:
99 _repo = _repo.rstrip('/')
99 _repo = _repo.rstrip('/')
100 return _repo
100 return _repo
101
101
102
102
103 def get_repo_group_slug(request):
103 def get_repo_group_slug(request):
104 _group = request.environ['pylons.routes_dict'].get('group_name')
104 _group = request.environ['pylons.routes_dict'].get('group_name')
105 if _group:
105 if _group:
106 _group = _group.rstrip('/')
106 _group = _group.rstrip('/')
107 return _group
107 return _group
108
108
109
109
110 def get_user_group_slug(request):
110 def get_user_group_slug(request):
111 _group = request.environ['pylons.routes_dict'].get('id')
111 _group = request.environ['pylons.routes_dict'].get('id')
112 _group = UserGroup.get(_group)
112 _group = UserGroup.get(_group)
113 if _group:
113 if _group:
114 return _group.users_group_name
114 return _group.users_group_name
115 return None
115 return None
116
116
117
117
118 def _extract_id_from_repo_name(repo_name):
118 def _extract_id_from_repo_name(repo_name):
119 if repo_name.startswith('/'):
119 if repo_name.startswith('/'):
120 repo_name = repo_name.lstrip('/')
120 repo_name = repo_name.lstrip('/')
121 by_id_match = re.match(r'^_(\d{1,})', repo_name)
121 by_id_match = re.match(r'^_(\d{1,})', repo_name)
122 if by_id_match:
122 if by_id_match:
123 return by_id_match.groups()[0]
123 return by_id_match.groups()[0]
124
124
125
125
126 def get_repo_by_id(repo_name):
126 def get_repo_by_id(repo_name):
127 """
127 """
128 Extracts repo_name by id from special urls. Example url is _11/repo_name
128 Extracts repo_name by id from special urls. Example url is _11/repo_name
129
129
130 :param repo_name:
130 :param repo_name:
131 :return: repo_name if matched else None
131 :return: repo_name if matched else None
132 """
132 """
133 _repo_id = _extract_id_from_repo_name(repo_name)
133 _repo_id = _extract_id_from_repo_name(repo_name)
134 if _repo_id:
134 if _repo_id:
135 from kallithea.model.db import Repository
135 from kallithea.model.db import Repository
136 repo = Repository.get(_repo_id)
136 repo = Repository.get(_repo_id)
137 if repo:
137 if repo:
138 # TODO: return repo instead of reponame? or would that be a layering violation?
138 # TODO: return repo instead of reponame? or would that be a layering violation?
139 return repo.repo_name
139 return repo.repo_name
140 return None
140 return None
141
141
142
142
143 def action_logger(user, action, repo, ipaddr='', commit=False):
143 def action_logger(user, action, repo, ipaddr='', commit=False):
144 """
144 """
145 Action logger for various actions made by users
145 Action logger for various actions made by users
146
146
147 :param user: user that made this action, can be a unique username string or
147 :param user: user that made this action, can be a unique username string or
148 object containing user_id attribute
148 object containing user_id attribute
149 :param action: action to log, should be on of predefined unique actions for
149 :param action: action to log, should be on of predefined unique actions for
150 easy translations
150 easy translations
151 :param repo: string name of repository or object containing repo_id,
151 :param repo: string name of repository or object containing repo_id,
152 that action was made on
152 that action was made on
153 :param ipaddr: optional IP address from what the action was made
153 :param ipaddr: optional IP address from what the action was made
154
154
155 """
155 """
156
156
157 # if we don't get explicit IP address try to get one from registered user
157 # if we don't get explicit IP address try to get one from registered user
158 # in tmpl context var
158 # in tmpl context var
159 if not ipaddr:
159 if not ipaddr:
160 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
160 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
161
161
162 if getattr(user, 'user_id', None):
162 if getattr(user, 'user_id', None):
163 user_obj = User.get(user.user_id)
163 user_obj = User.get(user.user_id)
164 elif isinstance(user, basestring):
164 elif isinstance(user, basestring):
165 user_obj = User.get_by_username(user)
165 user_obj = User.get_by_username(user)
166 else:
166 else:
167 raise Exception('You have to provide a user object or a username')
167 raise Exception('You have to provide a user object or a username')
168
168
169 if getattr(repo, 'repo_id', None):
169 if getattr(repo, 'repo_id', None):
170 repo_obj = Repository.get(repo.repo_id)
170 repo_obj = Repository.get(repo.repo_id)
171 repo_name = repo_obj.repo_name
171 repo_name = repo_obj.repo_name
172 elif isinstance(repo, basestring):
172 elif isinstance(repo, basestring):
173 repo_name = repo.lstrip('/')
173 repo_name = repo.lstrip('/')
174 repo_obj = Repository.get_by_repo_name(repo_name)
174 repo_obj = Repository.get_by_repo_name(repo_name)
175 else:
175 else:
176 repo_obj = None
176 repo_obj = None
177 repo_name = u''
177 repo_name = u''
178
178
179 user_log = UserLog()
179 user_log = UserLog()
180 user_log.user_id = user_obj.user_id
180 user_log.user_id = user_obj.user_id
181 user_log.username = user_obj.username
181 user_log.username = user_obj.username
182 user_log.action = safe_unicode(action)
182 user_log.action = safe_unicode(action)
183
183
184 user_log.repository = repo_obj
184 user_log.repository = repo_obj
185 user_log.repository_name = repo_name
185 user_log.repository_name = repo_name
186
186
187 user_log.action_date = datetime.datetime.now()
187 user_log.action_date = datetime.datetime.now()
188 user_log.user_ip = ipaddr
188 user_log.user_ip = ipaddr
189 meta.Session().add(user_log)
189 meta.Session().add(user_log)
190
190
191 log.info('Logging action:%s on %s by user:%s ip:%s',
191 log.info('Logging action:%s on %s by user:%s ip:%s',
192 action, safe_unicode(repo), user_obj, ipaddr)
192 action, safe_unicode(repo), user_obj, ipaddr)
193 if commit:
193 if commit:
194 meta.Session().commit()
194 meta.Session().commit()
195
195
196
196
197 def get_filesystem_repos(path):
197 def get_filesystem_repos(path):
198 """
198 """
199 Scans given path for repos and return (name,(type,path)) tuple
199 Scans given path for repos and return (name,(type,path)) tuple
200
200
201 :param path: path to scan for repositories
201 :param path: path to scan for repositories
202 :param recursive: recursive search and return names with subdirs in front
202 :param recursive: recursive search and return names with subdirs in front
203 """
203 """
204
204
205 # remove ending slash for better results
205 # remove ending slash for better results
206 path = safe_str(path.rstrip(os.sep))
206 path = safe_str(path.rstrip(os.sep))
207 log.debug('now scanning in %s', path)
207 log.debug('now scanning in %s', path)
208
208
209 def isdir(*n):
209 def isdir(*n):
210 return os.path.isdir(os.path.join(*n))
210 return os.path.isdir(os.path.join(*n))
211
211
212 for root, dirs, _files in os.walk(path):
212 for root, dirs, _files in os.walk(path):
213 recurse_dirs = []
213 recurse_dirs = []
214 for subdir in dirs:
214 for subdir in dirs:
215 # skip removed repos
215 # skip removed repos
216 if REMOVED_REPO_PAT.match(subdir):
216 if REMOVED_REPO_PAT.match(subdir):
217 continue
217 continue
218
218
219 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
219 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
220 if subdir.startswith('.'):
220 if subdir.startswith('.'):
221 continue
221 continue
222
222
223 cur_path = os.path.join(root, subdir)
223 cur_path = os.path.join(root, subdir)
224 if isdir(cur_path, '.git'):
224 if isdir(cur_path, '.git'):
225 log.warning('ignoring non-bare Git repo: %s', cur_path)
225 log.warning('ignoring non-bare Git repo: %s', cur_path)
226 continue
226 continue
227
227
228 if (isdir(cur_path, '.hg') or
228 if (isdir(cur_path, '.hg') or
229 isdir(cur_path, '.svn') or
229 isdir(cur_path, '.svn') or
230 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
230 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
231 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
231 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
232
232
233 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
233 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
234 log.warning('ignoring repo path without access: %s', cur_path)
234 log.warning('ignoring repo path without access: %s', cur_path)
235 continue
235 continue
236
236
237 if not os.access(cur_path, os.W_OK):
237 if not os.access(cur_path, os.W_OK):
238 log.warning('repo path without write access: %s', cur_path)
238 log.warning('repo path without write access: %s', cur_path)
239
239
240 try:
240 try:
241 scm_info = get_scm(cur_path)
241 scm_info = get_scm(cur_path)
242 assert cur_path.startswith(path)
242 assert cur_path.startswith(path)
243 repo_path = cur_path[len(path) + 1:]
243 repo_path = cur_path[len(path) + 1:]
244 yield repo_path, scm_info
244 yield repo_path, scm_info
245 continue # no recursion
245 continue # no recursion
246 except VCSError:
246 except VCSError:
247 # We should perhaps ignore such broken repos, but especially
247 # We should perhaps ignore such broken repos, but especially
248 # the bare git detection is unreliable so we dive into it
248 # the bare git detection is unreliable so we dive into it
249 pass
249 pass
250
250
251 recurse_dirs.append(subdir)
251 recurse_dirs.append(subdir)
252
252
253 dirs[:] = recurse_dirs
253 dirs[:] = recurse_dirs
254
254
255
255
256 def is_valid_repo_uri(repo_type, url, ui):
256 def is_valid_repo_uri(repo_type, url, ui):
257 """Check if the url seems like a valid remote repo location - raise an Exception if any problems"""
257 """Check if the url seems like a valid remote repo location - raise an Exception if any problems"""
258 if repo_type == 'hg':
258 if repo_type == 'hg':
259 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
259 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
260 if url.startswith('http') or url.startswith('ssh'):
260 if url.startswith('http') or url.startswith('ssh'):
261 # initially check if it's at least the proper URL
261 # initially check if it's at least the proper URL
262 # or does it pass basic auth
262 # or does it pass basic auth
263 MercurialRepository._check_url(url, ui)
263 MercurialRepository._check_url(url, ui)
264 elif url.startswith('svn+http'):
264 elif url.startswith('svn+http'):
265 try:
265 try:
266 from hgsubversion.svnrepo import svnremoterepo
266 from hgsubversion.svnrepo import svnremoterepo
267 except ImportError:
267 except ImportError:
268 raise HgsubversionImportError(_('Unable to activate hgsubversion support. '
268 raise HgsubversionImportError(_('Unable to activate hgsubversion support. '
269 'The "hgsubversion" library is missing'))
269 'The "hgsubversion" library is missing'))
270 svnremoterepo(ui, url).svn.uuid
270 svnremoterepo(ui, url).svn.uuid
271 elif url.startswith('git+http'):
271 elif url.startswith('git+http'):
272 raise NotImplementedError()
272 raise NotImplementedError()
273 else:
273 else:
274 raise Exception('URI %s not allowed' % (url,))
274 raise Exception('URI %s not allowed' % (url,))
275
275
276 elif repo_type == 'git':
276 elif repo_type == 'git':
277 from kallithea.lib.vcs.backends.git.repository import GitRepository
277 from kallithea.lib.vcs.backends.git.repository import GitRepository
278 if url.startswith('http') or url.startswith('git'):
278 if url.startswith('http') or url.startswith('git'):
279 # initially check if it's at least the proper URL
279 # initially check if it's at least the proper URL
280 # or does it pass basic auth
280 # or does it pass basic auth
281 GitRepository._check_url(url)
281 GitRepository._check_url(url)
282 elif url.startswith('svn+http'):
282 elif url.startswith('svn+http'):
283 raise NotImplementedError()
283 raise NotImplementedError()
284 elif url.startswith('hg+http'):
284 elif url.startswith('hg+http'):
285 raise NotImplementedError()
285 raise NotImplementedError()
286 else:
286 else:
287 raise Exception('URI %s not allowed' % (url))
287 raise Exception('URI %s not allowed' % (url))
288
288
289
289
290 def is_valid_repo(repo_name, base_path, scm=None):
290 def is_valid_repo(repo_name, base_path, scm=None):
291 """
291 """
292 Returns True if given path is a valid repository False otherwise.
292 Returns True if given path is a valid repository False otherwise.
293 If scm param is given also compare if given scm is the same as expected
293 If scm param is given also compare if given scm is the same as expected
294 from scm parameter
294 from scm parameter
295
295
296 :param repo_name:
296 :param repo_name:
297 :param base_path:
297 :param base_path:
298 :param scm:
298 :param scm:
299
299
300 :return True: if given path is a valid repository
300 :return True: if given path is a valid repository
301 """
301 """
302 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
302 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
303
303
304 try:
304 try:
305 scm_ = get_scm(full_path)
305 scm_ = get_scm(full_path)
306 if scm:
306 if scm:
307 return scm_[0] == scm
307 return scm_[0] == scm
308 return True
308 return True
309 except VCSError:
309 except VCSError:
310 return False
310 return False
311
311
312
312
313 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
313 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
314 """
314 """
315 Returns True if given path is a repository group False otherwise
315 Returns True if given path is a repository group False otherwise
316
316
317 :param repo_name:
317 :param repo_name:
318 :param base_path:
318 :param base_path:
319 """
319 """
320 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
320 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
321
321
322 # check if it's not a repo
322 # check if it's not a repo
323 if is_valid_repo(repo_group_name, base_path):
323 if is_valid_repo(repo_group_name, base_path):
324 return False
324 return False
325
325
326 try:
326 try:
327 # we need to check bare git repos at higher level
327 # we need to check bare git repos at higher level
328 # since we might match branches/hooks/info/objects or possible
328 # since we might match branches/hooks/info/objects or possible
329 # other things inside bare git repo
329 # other things inside bare git repo
330 get_scm(os.path.dirname(full_path))
330 get_scm(os.path.dirname(full_path))
331 return False
331 return False
332 except VCSError:
332 except VCSError:
333 pass
333 pass
334
334
335 # check if it's a valid path
335 # check if it's a valid path
336 if skip_path_check or os.path.isdir(full_path):
336 if skip_path_check or os.path.isdir(full_path):
337 return True
337 return True
338
338
339 return False
339 return False
340
340
341
341
342 # propagated from mercurial documentation
342 # propagated from mercurial documentation
343 ui_sections = ['alias', 'auth',
343 ui_sections = ['alias', 'auth',
344 'decode/encode', 'defaults',
344 'decode/encode', 'defaults',
345 'diff', 'email',
345 'diff', 'email',
346 'extensions', 'format',
346 'extensions', 'format',
347 'merge-patterns', 'merge-tools',
347 'merge-patterns', 'merge-tools',
348 'hooks', 'http_proxy',
348 'hooks', 'http_proxy',
349 'smtp', 'patch',
349 'smtp', 'patch',
350 'paths', 'profiling',
350 'paths', 'profiling',
351 'server', 'trusted',
351 'server', 'trusted',
352 'ui', 'web', ]
352 'ui', 'web', ]
353
353
354
354
355 def make_ui(read_from='file', path=None, clear_session=True):
355 def make_ui(read_from='file', path=None, clear_session=True):
356 """
356 """
357 A function that will read python rc files or database
357 A function that will read python rc files or database
358 and make an mercurial ui object from read options
358 and make an mercurial ui object from read options
359
359
360 :param path: path to mercurial config file
360 :param path: path to mercurial config file
361 :param read_from: read from 'file' or 'db'
361 :param read_from: read from 'file' or 'db'
362 """
362 """
363
363
364 baseui = ui.ui()
364 baseui = ui.ui()
365
365
366 # clean the baseui object
366 # clean the baseui object
367 baseui._ocfg = config.config()
367 baseui._ocfg = config.config()
368 baseui._ucfg = config.config()
368 baseui._ucfg = config.config()
369 baseui._tcfg = config.config()
369 baseui._tcfg = config.config()
370
370
371 if read_from == 'file':
371 if read_from == 'file':
372 if not os.path.isfile(path):
372 if not os.path.isfile(path):
373 log.debug('hgrc file is not present at %s, skipping...', path)
373 log.debug('hgrc file is not present at %s, skipping...', path)
374 return False
374 return False
375 log.debug('reading hgrc from %s', path)
375 log.debug('reading hgrc from %s', path)
376 cfg = config.config()
376 cfg = config.config()
377 cfg.read(path)
377 cfg.read(path)
378 for section in ui_sections:
378 for section in ui_sections:
379 for k, v in cfg.items(section):
379 for k, v in cfg.items(section):
380 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
380 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
381 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
381 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
382
382
383 elif read_from == 'db':
383 elif read_from == 'db':
384 sa = meta.Session()
384 sa = meta.Session()
385 ret = sa.query(Ui).all()
385 ret = sa.query(Ui).all()
386
386
387 hg_ui = ret
387 hg_ui = ret
388 for ui_ in hg_ui:
388 for ui_ in hg_ui:
389 if ui_.ui_active:
389 if ui_.ui_active:
390 ui_val = '' if ui_.ui_value is None else safe_str(ui_.ui_value)
390 ui_val = '' if ui_.ui_value is None else safe_str(ui_.ui_value)
391 log.debug('settings ui from db: [%s] %s=%r', ui_.ui_section,
391 log.debug('settings ui from db: [%s] %s=%r', ui_.ui_section,
392 ui_.ui_key, ui_val)
392 ui_.ui_key, ui_val)
393 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
393 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
394 ui_val)
394 ui_val)
395 if clear_session:
395 if clear_session:
396 meta.Session.remove()
396 meta.Session.remove()
397
397
398 # force set push_ssl requirement to False, Kallithea handles that
398 # force set push_ssl requirement to False, Kallithea handles that
399 baseui.setconfig('web', 'push_ssl', False)
399 baseui.setconfig('web', 'push_ssl', False)
400 baseui.setconfig('web', 'allow_push', '*')
400 baseui.setconfig('web', 'allow_push', '*')
401 # prevent interactive questions for ssh password / passphrase
401 # prevent interactive questions for ssh password / passphrase
402 ssh = baseui.config('ui', 'ssh', default='ssh')
402 ssh = baseui.config('ui', 'ssh', default='ssh')
403 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
403 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
404
404
405 return baseui
405 return baseui
406
406
407
407
408 def set_app_settings(config):
408 def set_app_settings(config):
409 """
409 """
410 Updates app config with new settings from database
410 Updates app config with new settings from database
411
411
412 :param config:
412 :param config:
413 """
413 """
414 try:
414 try:
415 hgsettings = Setting.get_app_settings()
415 hgsettings = Setting.get_app_settings()
416 for k, v in hgsettings.items():
416 for k, v in hgsettings.items():
417 config[k] = v
417 config[k] = v
418 finally:
418 finally:
419 meta.Session.remove()
419 meta.Session.remove()
420
420
421
421
422 def set_vcs_config(config):
422 def set_vcs_config(config):
423 """
423 """
424 Patch VCS config with some Kallithea specific stuff
424 Patch VCS config with some Kallithea specific stuff
425
425
426 :param config: kallithea.CONFIG
426 :param config: kallithea.CONFIG
427 """
427 """
428 from kallithea.lib.vcs import conf
428 from kallithea.lib.vcs import conf
429 from kallithea.lib.utils2 import aslist
429 from kallithea.lib.utils2 import aslist
430 conf.settings.BACKENDS = {
430 conf.settings.BACKENDS = {
431 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
431 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
432 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
432 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
433 }
433 }
434
434
435 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
435 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
436 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
436 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
437 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
437 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
438 'utf8'), sep=',')
438 'utf-8'), sep=',')
439
439
440
440
441 def set_indexer_config(config):
441 def set_indexer_config(config):
442 """
442 """
443 Update Whoosh index mapping
443 Update Whoosh index mapping
444
444
445 :param config: kallithea.CONFIG
445 :param config: kallithea.CONFIG
446 """
446 """
447 from kallithea.config import conf
447 from kallithea.config import conf
448
448
449 log.debug('adding extra into INDEX_EXTENSIONS')
449 log.debug('adding extra into INDEX_EXTENSIONS')
450 conf.INDEX_EXTENSIONS.extend(re.split('\s+', config.get('index.extensions', '')))
450 conf.INDEX_EXTENSIONS.extend(re.split('\s+', config.get('index.extensions', '')))
451
451
452 log.debug('adding extra into INDEX_FILENAMES')
452 log.debug('adding extra into INDEX_FILENAMES')
453 conf.INDEX_FILENAMES.extend(re.split('\s+', config.get('index.filenames', '')))
453 conf.INDEX_FILENAMES.extend(re.split('\s+', config.get('index.filenames', '')))
454
454
455
455
456 def map_groups(path):
456 def map_groups(path):
457 """
457 """
458 Given a full path to a repository, create all nested groups that this
458 Given a full path to a repository, create all nested groups that this
459 repo is inside. This function creates parent-child relationships between
459 repo is inside. This function creates parent-child relationships between
460 groups and creates default perms for all new groups.
460 groups and creates default perms for all new groups.
461
461
462 :param paths: full path to repository
462 :param paths: full path to repository
463 """
463 """
464 sa = meta.Session()
464 sa = meta.Session()
465 groups = path.split(Repository.url_sep())
465 groups = path.split(Repository.url_sep())
466 parent = None
466 parent = None
467 group = None
467 group = None
468
468
469 # last element is repo in nested groups structure
469 # last element is repo in nested groups structure
470 groups = groups[:-1]
470 groups = groups[:-1]
471 rgm = RepoGroupModel()
471 rgm = RepoGroupModel()
472 owner = User.get_first_admin()
472 owner = User.get_first_admin()
473 for lvl, group_name in enumerate(groups):
473 for lvl, group_name in enumerate(groups):
474 group_name = u'/'.join(groups[:lvl] + [group_name])
474 group_name = u'/'.join(groups[:lvl] + [group_name])
475 group = RepoGroup.get_by_group_name(group_name)
475 group = RepoGroup.get_by_group_name(group_name)
476 desc = '%s group' % group_name
476 desc = '%s group' % group_name
477
477
478 # skip folders that are now removed repos
478 # skip folders that are now removed repos
479 if REMOVED_REPO_PAT.match(group_name):
479 if REMOVED_REPO_PAT.match(group_name):
480 break
480 break
481
481
482 if group is None:
482 if group is None:
483 log.debug('creating group level: %s group_name: %s',
483 log.debug('creating group level: %s group_name: %s',
484 lvl, group_name)
484 lvl, group_name)
485 group = RepoGroup(group_name, parent)
485 group = RepoGroup(group_name, parent)
486 group.group_description = desc
486 group.group_description = desc
487 group.owner = owner
487 group.owner = owner
488 sa.add(group)
488 sa.add(group)
489 rgm._create_default_perms(group)
489 rgm._create_default_perms(group)
490 sa.flush()
490 sa.flush()
491
491
492 parent = group
492 parent = group
493 return group
493 return group
494
494
495
495
496 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
496 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
497 install_git_hooks=False, user=None, overwrite_git_hooks=False):
497 install_git_hooks=False, user=None, overwrite_git_hooks=False):
498 """
498 """
499 maps all repos given in initial_repo_list, non existing repositories
499 maps all repos given in initial_repo_list, non existing repositories
500 are created, if remove_obsolete is True it also check for db entries
500 are created, if remove_obsolete is True it also check for db entries
501 that are not in initial_repo_list and removes them.
501 that are not in initial_repo_list and removes them.
502
502
503 :param initial_repo_list: list of repositories found by scanning methods
503 :param initial_repo_list: list of repositories found by scanning methods
504 :param remove_obsolete: check for obsolete entries in database
504 :param remove_obsolete: check for obsolete entries in database
505 :param install_git_hooks: if this is True, also check and install git hook
505 :param install_git_hooks: if this is True, also check and install git hook
506 for a repo if missing
506 for a repo if missing
507 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
507 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
508 that may be encountered (even if user-deployed)
508 that may be encountered (even if user-deployed)
509 """
509 """
510 from kallithea.model.repo import RepoModel
510 from kallithea.model.repo import RepoModel
511 from kallithea.model.scm import ScmModel
511 from kallithea.model.scm import ScmModel
512 sa = meta.Session()
512 sa = meta.Session()
513 repo_model = RepoModel()
513 repo_model = RepoModel()
514 if user is None:
514 if user is None:
515 user = User.get_first_admin()
515 user = User.get_first_admin()
516 added = []
516 added = []
517
517
518 # creation defaults
518 # creation defaults
519 defs = Setting.get_default_repo_settings(strip_prefix=True)
519 defs = Setting.get_default_repo_settings(strip_prefix=True)
520 enable_statistics = defs.get('repo_enable_statistics')
520 enable_statistics = defs.get('repo_enable_statistics')
521 enable_locking = defs.get('repo_enable_locking')
521 enable_locking = defs.get('repo_enable_locking')
522 enable_downloads = defs.get('repo_enable_downloads')
522 enable_downloads = defs.get('repo_enable_downloads')
523 private = defs.get('repo_private')
523 private = defs.get('repo_private')
524
524
525 for name, repo in initial_repo_list.items():
525 for name, repo in initial_repo_list.items():
526 group = map_groups(name)
526 group = map_groups(name)
527 unicode_name = safe_unicode(name)
527 unicode_name = safe_unicode(name)
528 db_repo = repo_model.get_by_repo_name(unicode_name)
528 db_repo = repo_model.get_by_repo_name(unicode_name)
529 # found repo that is on filesystem not in Kallithea database
529 # found repo that is on filesystem not in Kallithea database
530 if not db_repo:
530 if not db_repo:
531 log.info('repository %s not found, creating now', name)
531 log.info('repository %s not found, creating now', name)
532 added.append(name)
532 added.append(name)
533 desc = (repo.description
533 desc = (repo.description
534 if repo.description != 'unknown'
534 if repo.description != 'unknown'
535 else '%s repository' % name)
535 else '%s repository' % name)
536
536
537 new_repo = repo_model._create_repo(
537 new_repo = repo_model._create_repo(
538 repo_name=name,
538 repo_name=name,
539 repo_type=repo.alias,
539 repo_type=repo.alias,
540 description=desc,
540 description=desc,
541 repo_group=getattr(group, 'group_id', None),
541 repo_group=getattr(group, 'group_id', None),
542 owner=user,
542 owner=user,
543 enable_locking=enable_locking,
543 enable_locking=enable_locking,
544 enable_downloads=enable_downloads,
544 enable_downloads=enable_downloads,
545 enable_statistics=enable_statistics,
545 enable_statistics=enable_statistics,
546 private=private,
546 private=private,
547 state=Repository.STATE_CREATED
547 state=Repository.STATE_CREATED
548 )
548 )
549 sa.commit()
549 sa.commit()
550 # we added that repo just now, and make sure it has githook
550 # we added that repo just now, and make sure it has githook
551 # installed, and updated server info
551 # installed, and updated server info
552 if new_repo.repo_type == 'git':
552 if new_repo.repo_type == 'git':
553 git_repo = new_repo.scm_instance
553 git_repo = new_repo.scm_instance
554 ScmModel().install_git_hooks(git_repo)
554 ScmModel().install_git_hooks(git_repo)
555 # update repository server-info
555 # update repository server-info
556 log.debug('Running update server info')
556 log.debug('Running update server info')
557 git_repo._update_server_info()
557 git_repo._update_server_info()
558 new_repo.update_changeset_cache()
558 new_repo.update_changeset_cache()
559 elif install_git_hooks:
559 elif install_git_hooks:
560 if db_repo.repo_type == 'git':
560 if db_repo.repo_type == 'git':
561 ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
561 ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
562
562
563 removed = []
563 removed = []
564 # remove from database those repositories that are not in the filesystem
564 # remove from database those repositories that are not in the filesystem
565 unicode_initial_repo_list = set(safe_unicode(name) for name in initial_repo_list)
565 unicode_initial_repo_list = set(safe_unicode(name) for name in initial_repo_list)
566 for repo in sa.query(Repository).all():
566 for repo in sa.query(Repository).all():
567 if repo.repo_name not in unicode_initial_repo_list:
567 if repo.repo_name not in unicode_initial_repo_list:
568 if remove_obsolete:
568 if remove_obsolete:
569 log.debug("Removing non-existing repository found in db `%s`",
569 log.debug("Removing non-existing repository found in db `%s`",
570 repo.repo_name)
570 repo.repo_name)
571 try:
571 try:
572 RepoModel().delete(repo, forks='detach', fs_remove=False)
572 RepoModel().delete(repo, forks='detach', fs_remove=False)
573 sa.commit()
573 sa.commit()
574 except Exception:
574 except Exception:
575 #don't hold further removals on error
575 #don't hold further removals on error
576 log.error(traceback.format_exc())
576 log.error(traceback.format_exc())
577 sa.rollback()
577 sa.rollback()
578 removed.append(repo.repo_name)
578 removed.append(repo.repo_name)
579 return added, removed
579 return added, removed
580
580
581
581
582 def load_rcextensions(root_path):
582 def load_rcextensions(root_path):
583 import kallithea
583 import kallithea
584 from kallithea.config import conf
584 from kallithea.config import conf
585
585
586 path = os.path.join(root_path, 'rcextensions', '__init__.py')
586 path = os.path.join(root_path, 'rcextensions', '__init__.py')
587 if os.path.isfile(path):
587 if os.path.isfile(path):
588 rcext = create_module('rc', path)
588 rcext = create_module('rc', path)
589 EXT = kallithea.EXTENSIONS = rcext
589 EXT = kallithea.EXTENSIONS = rcext
590 log.debug('Found rcextensions now loading %s...', rcext)
590 log.debug('Found rcextensions now loading %s...', rcext)
591
591
592 # Additional mappings that are not present in the pygments lexers
592 # Additional mappings that are not present in the pygments lexers
593 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
593 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
594
594
595 # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
595 # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
596
596
597 if getattr(EXT, 'INDEX_EXTENSIONS', []):
597 if getattr(EXT, 'INDEX_EXTENSIONS', []):
598 log.debug('settings custom INDEX_EXTENSIONS')
598 log.debug('settings custom INDEX_EXTENSIONS')
599 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
599 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
600
600
601 # ADDITIONAL MAPPINGS
601 # ADDITIONAL MAPPINGS
602 log.debug('adding extra into INDEX_EXTENSIONS')
602 log.debug('adding extra into INDEX_EXTENSIONS')
603 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
603 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
604
604
605 # auto check if the module is not missing any data, set to default if is
605 # auto check if the module is not missing any data, set to default if is
606 # this will help autoupdate new feature of rcext module
606 # this will help autoupdate new feature of rcext module
607 #from kallithea.config import rcextensions
607 #from kallithea.config import rcextensions
608 #for k in dir(rcextensions):
608 #for k in dir(rcextensions):
609 # if not k.startswith('_') and not hasattr(EXT, k):
609 # if not k.startswith('_') and not hasattr(EXT, k):
610 # setattr(EXT, k, getattr(rcextensions, k))
610 # setattr(EXT, k, getattr(rcextensions, k))
611
611
612
612
613 #==============================================================================
613 #==============================================================================
614 # MISC
614 # MISC
615 #==============================================================================
615 #==============================================================================
616
616
617 def check_git_version():
617 def check_git_version():
618 """
618 """
619 Checks what version of git is installed in system, and issues a warning
619 Checks what version of git is installed in system, and issues a warning
620 if it's too old for Kallithea to work properly.
620 if it's too old for Kallithea to work properly.
621 """
621 """
622 from kallithea import BACKENDS
622 from kallithea import BACKENDS
623 from kallithea.lib.vcs.backends.git.repository import GitRepository
623 from kallithea.lib.vcs.backends.git.repository import GitRepository
624 from kallithea.lib.vcs.conf import settings
624 from kallithea.lib.vcs.conf import settings
625 from distutils.version import StrictVersion
625 from distutils.version import StrictVersion
626
626
627 if 'git' not in BACKENDS:
627 if 'git' not in BACKENDS:
628 return None
628 return None
629
629
630 stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
630 stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
631 _safe=True)
631 _safe=True)
632
632
633 m = re.search("\d+.\d+.\d+", stdout)
633 m = re.search("\d+.\d+.\d+", stdout)
634 if m:
634 if m:
635 ver = StrictVersion(m.group(0))
635 ver = StrictVersion(m.group(0))
636 else:
636 else:
637 ver = StrictVersion('0.0.0')
637 ver = StrictVersion('0.0.0')
638
638
639 req_ver = StrictVersion('1.7.4')
639 req_ver = StrictVersion('1.7.4')
640
640
641 log.debug('Git executable: "%s" version %s detected: %s',
641 log.debug('Git executable: "%s" version %s detected: %s',
642 settings.GIT_EXECUTABLE_PATH, ver, stdout)
642 settings.GIT_EXECUTABLE_PATH, ver, stdout)
643 if stderr:
643 if stderr:
644 log.warning('Error detecting git version: %r', stderr)
644 log.warning('Error detecting git version: %r', stderr)
645 elif ver < req_ver:
645 elif ver < req_ver:
646 log.warning('Kallithea detected git version %s, which is too old '
646 log.warning('Kallithea detected git version %s, which is too old '
647 'for the system to function properly. '
647 'for the system to function properly. '
648 'Please upgrade to version %s or later.' % (ver, req_ver))
648 'Please upgrade to version %s or later.' % (ver, req_ver))
649 return ver
649 return ver
650
650
651
651
652 #===============================================================================
652 #===============================================================================
653 # CACHE RELATED METHODS
653 # CACHE RELATED METHODS
654 #===============================================================================
654 #===============================================================================
655
655
656 # set cache regions for beaker so celery can utilise it
656 # set cache regions for beaker so celery can utilise it
657 def setup_cache_regions(settings):
657 def setup_cache_regions(settings):
658 # Create dict with just beaker cache configs with prefix stripped
658 # Create dict with just beaker cache configs with prefix stripped
659 cache_settings = {'regions': None}
659 cache_settings = {'regions': None}
660 prefix = 'beaker.cache.'
660 prefix = 'beaker.cache.'
661 for key in settings:
661 for key in settings:
662 if key.startswith(prefix):
662 if key.startswith(prefix):
663 name = key[len(prefix):]
663 name = key[len(prefix):]
664 cache_settings[name] = settings[key]
664 cache_settings[name] = settings[key]
665 # Find all regions, apply defaults, and apply to beaker
665 # Find all regions, apply defaults, and apply to beaker
666 if cache_settings['regions']:
666 if cache_settings['regions']:
667 for region in cache_settings['regions'].split(','):
667 for region in cache_settings['regions'].split(','):
668 region = region.strip()
668 region = region.strip()
669 prefix = region + '.'
669 prefix = region + '.'
670 region_settings = {}
670 region_settings = {}
671 for key in cache_settings:
671 for key in cache_settings:
672 if key.startswith(prefix):
672 if key.startswith(prefix):
673 name = key[len(prefix):]
673 name = key[len(prefix):]
674 region_settings[name] = cache_settings[key]
674 region_settings[name] = cache_settings[key]
675 region_settings.setdefault('expire',
675 region_settings.setdefault('expire',
676 cache_settings.get('expire', '60'))
676 cache_settings.get('expire', '60'))
677 region_settings.setdefault('lock_dir',
677 region_settings.setdefault('lock_dir',
678 cache_settings.get('lock_dir'))
678 cache_settings.get('lock_dir'))
679 region_settings.setdefault('data_dir',
679 region_settings.setdefault('data_dir',
680 cache_settings.get('data_dir'))
680 cache_settings.get('data_dir'))
681 region_settings.setdefault('type',
681 region_settings.setdefault('type',
682 cache_settings.get('type', 'memory'))
682 cache_settings.get('type', 'memory'))
683 beaker.cache.cache_regions[region] = region_settings
683 beaker.cache.cache_regions[region] = region_settings
684
684
685
685
686 def conditional_cache(region, prefix, condition, func):
686 def conditional_cache(region, prefix, condition, func):
687 """
687 """
688
688
689 Conditional caching function use like::
689 Conditional caching function use like::
690 def _c(arg):
690 def _c(arg):
691 #heavy computation function
691 #heavy computation function
692 return data
692 return data
693
693
694 # depending from condition the compute is wrapped in cache or not
694 # depending from condition the compute is wrapped in cache or not
695 compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func)
695 compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func)
696 return compute(arg)
696 return compute(arg)
697
697
698 :param region: name of cache region
698 :param region: name of cache region
699 :param prefix: cache region prefix
699 :param prefix: cache region prefix
700 :param condition: condition for cache to be triggered, and return data cached
700 :param condition: condition for cache to be triggered, and return data cached
701 :param func: wrapped heavy function to compute
701 :param func: wrapped heavy function to compute
702
702
703 """
703 """
704 wrapped = func
704 wrapped = func
705 if condition:
705 if condition:
706 log.debug('conditional_cache: True, wrapping call of '
706 log.debug('conditional_cache: True, wrapping call of '
707 'func: %s into %s region cache' % (region, func))
707 'func: %s into %s region cache' % (region, func))
708 wrapped = _cache_decorate((prefix,), None, None, region)(func)
708 wrapped = _cache_decorate((prefix,), None, None, region)(func)
709
709
710 return wrapped
710 return wrapped
@@ -1,648 +1,648 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils2
15 kallithea.lib.utils2
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 Some simple helper functions
18 Some simple helper functions
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jan 5, 2011
22 :created_on: Jan 5, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import os
29 import os
30 import re
30 import re
31 import sys
31 import sys
32 import time
32 import time
33 import uuid
33 import uuid
34 import datetime
34 import datetime
35 import urllib
35 import urllib
36 import binascii
36 import binascii
37
37
38 import webob
38 import webob
39 import urlobject
39 import urlobject
40
40
41 from tg.i18n import ugettext as _, ungettext
41 from tg.i18n import ugettext as _, ungettext
42 from kallithea.lib.vcs.utils.lazy import LazyProperty
42 from kallithea.lib.vcs.utils.lazy import LazyProperty
43 from kallithea.lib.compat import json
43 from kallithea.lib.compat import json
44
44
45
45
46 def str2bool(_str):
46 def str2bool(_str):
47 """
47 """
48 returns True/False value from given string, it tries to translate the
48 returns True/False value from given string, it tries to translate the
49 string into boolean
49 string into boolean
50
50
51 :param _str: string value to translate into boolean
51 :param _str: string value to translate into boolean
52 :rtype: boolean
52 :rtype: boolean
53 :returns: boolean from given string
53 :returns: boolean from given string
54 """
54 """
55 if _str is None:
55 if _str is None:
56 return False
56 return False
57 if _str in (True, False):
57 if _str in (True, False):
58 return _str
58 return _str
59 _str = str(_str).strip().lower()
59 _str = str(_str).strip().lower()
60 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
60 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
61
61
62
62
63 def aslist(obj, sep=None, strip=True):
63 def aslist(obj, sep=None, strip=True):
64 """
64 """
65 Returns given string separated by sep as list
65 Returns given string separated by sep as list
66
66
67 :param obj:
67 :param obj:
68 :param sep:
68 :param sep:
69 :param strip:
69 :param strip:
70 """
70 """
71 if isinstance(obj, (basestring)):
71 if isinstance(obj, (basestring)):
72 lst = obj.split(sep)
72 lst = obj.split(sep)
73 if strip:
73 if strip:
74 lst = [v.strip() for v in lst]
74 lst = [v.strip() for v in lst]
75 return lst
75 return lst
76 elif isinstance(obj, (list, tuple)):
76 elif isinstance(obj, (list, tuple)):
77 return obj
77 return obj
78 elif obj is None:
78 elif obj is None:
79 return []
79 return []
80 else:
80 else:
81 return [obj]
81 return [obj]
82
82
83
83
84 def convert_line_endings(line, mode):
84 def convert_line_endings(line, mode):
85 """
85 """
86 Converts a given line "line end" according to given mode
86 Converts a given line "line end" according to given mode
87
87
88 Available modes are::
88 Available modes are::
89 0 - Unix
89 0 - Unix
90 1 - Mac
90 1 - Mac
91 2 - DOS
91 2 - DOS
92
92
93 :param line: given line to convert
93 :param line: given line to convert
94 :param mode: mode to convert to
94 :param mode: mode to convert to
95 :rtype: str
95 :rtype: str
96 :return: converted line according to mode
96 :return: converted line according to mode
97 """
97 """
98 from string import replace
98 from string import replace
99
99
100 if mode == 0:
100 if mode == 0:
101 line = replace(line, '\r\n', '\n')
101 line = replace(line, '\r\n', '\n')
102 line = replace(line, '\r', '\n')
102 line = replace(line, '\r', '\n')
103 elif mode == 1:
103 elif mode == 1:
104 line = replace(line, '\r\n', '\r')
104 line = replace(line, '\r\n', '\r')
105 line = replace(line, '\n', '\r')
105 line = replace(line, '\n', '\r')
106 elif mode == 2:
106 elif mode == 2:
107 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
107 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
108 return line
108 return line
109
109
110
110
111 def detect_mode(line, default):
111 def detect_mode(line, default):
112 """
112 """
113 Detects line break for given line, if line break couldn't be found
113 Detects line break for given line, if line break couldn't be found
114 given default value is returned
114 given default value is returned
115
115
116 :param line: str line
116 :param line: str line
117 :param default: default
117 :param default: default
118 :rtype: int
118 :rtype: int
119 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
119 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
120 """
120 """
121 if line.endswith('\r\n'):
121 if line.endswith('\r\n'):
122 return 2
122 return 2
123 elif line.endswith('\n'):
123 elif line.endswith('\n'):
124 return 0
124 return 0
125 elif line.endswith('\r'):
125 elif line.endswith('\r'):
126 return 1
126 return 1
127 else:
127 else:
128 return default
128 return default
129
129
130
130
131 def generate_api_key():
131 def generate_api_key():
132 """
132 """
133 Generates a random (presumably unique) API key.
133 Generates a random (presumably unique) API key.
134
134
135 This value is used in URLs and "Bearer" HTTP Authorization headers,
135 This value is used in URLs and "Bearer" HTTP Authorization headers,
136 which in practice means it should only contain URL-safe characters
136 which in practice means it should only contain URL-safe characters
137 (RFC 3986):
137 (RFC 3986):
138
138
139 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
139 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
140 """
140 """
141 # Hexadecimal certainly qualifies as URL-safe.
141 # Hexadecimal certainly qualifies as URL-safe.
142 return binascii.hexlify(os.urandom(20))
142 return binascii.hexlify(os.urandom(20))
143
143
144
144
145 def safe_int(val, default=None):
145 def safe_int(val, default=None):
146 """
146 """
147 Returns int() of val if val is not convertable to int use default
147 Returns int() of val if val is not convertable to int use default
148 instead
148 instead
149
149
150 :param val:
150 :param val:
151 :param default:
151 :param default:
152 """
152 """
153
153
154 try:
154 try:
155 val = int(val)
155 val = int(val)
156 except (ValueError, TypeError):
156 except (ValueError, TypeError):
157 val = default
157 val = default
158
158
159 return val
159 return val
160
160
161
161
162 def safe_unicode(str_, from_encoding=None):
162 def safe_unicode(str_, from_encoding=None):
163 """
163 """
164 safe unicode function. Does few trick to turn str_ into unicode
164 safe unicode function. Does few trick to turn str_ into unicode
165
165
166 In case of UnicodeDecode error we try to return it with encoding detected
166 In case of UnicodeDecode error we try to return it with encoding detected
167 by chardet library if it fails fallback to unicode with errors replaced
167 by chardet library if it fails fallback to unicode with errors replaced
168
168
169 :param str_: string to decode
169 :param str_: string to decode
170 :rtype: unicode
170 :rtype: unicode
171 :returns: unicode object
171 :returns: unicode object
172 """
172 """
173 if isinstance(str_, unicode):
173 if isinstance(str_, unicode):
174 return str_
174 return str_
175
175
176 if not from_encoding:
176 if not from_encoding:
177 import kallithea
177 import kallithea
178 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
178 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
179 'utf8'), sep=',')
179 'utf-8'), sep=',')
180 from_encoding = DEFAULT_ENCODINGS
180 from_encoding = DEFAULT_ENCODINGS
181
181
182 if not isinstance(from_encoding, (list, tuple)):
182 if not isinstance(from_encoding, (list, tuple)):
183 from_encoding = [from_encoding]
183 from_encoding = [from_encoding]
184
184
185 try:
185 try:
186 return unicode(str_)
186 return unicode(str_)
187 except UnicodeDecodeError:
187 except UnicodeDecodeError:
188 pass
188 pass
189
189
190 for enc in from_encoding:
190 for enc in from_encoding:
191 try:
191 try:
192 return unicode(str_, enc)
192 return unicode(str_, enc)
193 except UnicodeDecodeError:
193 except UnicodeDecodeError:
194 pass
194 pass
195
195
196 try:
196 try:
197 import chardet
197 import chardet
198 encoding = chardet.detect(str_)['encoding']
198 encoding = chardet.detect(str_)['encoding']
199 if encoding is None:
199 if encoding is None:
200 raise Exception()
200 raise Exception()
201 return str_.decode(encoding)
201 return str_.decode(encoding)
202 except (ImportError, UnicodeDecodeError, Exception):
202 except (ImportError, UnicodeDecodeError, Exception):
203 return unicode(str_, from_encoding[0], 'replace')
203 return unicode(str_, from_encoding[0], 'replace')
204
204
205
205
206 def safe_str(unicode_, to_encoding=None):
206 def safe_str(unicode_, to_encoding=None):
207 """
207 """
208 safe str function. Does few trick to turn unicode_ into string
208 safe str function. Does few trick to turn unicode_ into string
209
209
210 In case of UnicodeEncodeError we try to return it with encoding detected
210 In case of UnicodeEncodeError we try to return it with encoding detected
211 by chardet library if it fails fallback to string with errors replaced
211 by chardet library if it fails fallback to string with errors replaced
212
212
213 :param unicode_: unicode to encode
213 :param unicode_: unicode to encode
214 :rtype: str
214 :rtype: str
215 :returns: str object
215 :returns: str object
216 """
216 """
217
217
218 # if it's not basestr cast to str
218 # if it's not basestr cast to str
219 if not isinstance(unicode_, basestring):
219 if not isinstance(unicode_, basestring):
220 return str(unicode_)
220 return str(unicode_)
221
221
222 if isinstance(unicode_, str):
222 if isinstance(unicode_, str):
223 return unicode_
223 return unicode_
224
224
225 if not to_encoding:
225 if not to_encoding:
226 import kallithea
226 import kallithea
227 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
227 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
228 'utf8'), sep=',')
228 'utf-8'), sep=',')
229 to_encoding = DEFAULT_ENCODINGS
229 to_encoding = DEFAULT_ENCODINGS
230
230
231 if not isinstance(to_encoding, (list, tuple)):
231 if not isinstance(to_encoding, (list, tuple)):
232 to_encoding = [to_encoding]
232 to_encoding = [to_encoding]
233
233
234 for enc in to_encoding:
234 for enc in to_encoding:
235 try:
235 try:
236 return unicode_.encode(enc)
236 return unicode_.encode(enc)
237 except UnicodeEncodeError:
237 except UnicodeEncodeError:
238 pass
238 pass
239
239
240 try:
240 try:
241 import chardet
241 import chardet
242 encoding = chardet.detect(unicode_)['encoding']
242 encoding = chardet.detect(unicode_)['encoding']
243 if encoding is None:
243 if encoding is None:
244 raise UnicodeEncodeError()
244 raise UnicodeEncodeError()
245
245
246 return unicode_.encode(encoding)
246 return unicode_.encode(encoding)
247 except (ImportError, UnicodeEncodeError):
247 except (ImportError, UnicodeEncodeError):
248 return unicode_.encode(to_encoding[0], 'replace')
248 return unicode_.encode(to_encoding[0], 'replace')
249
249
250
250
251 def remove_suffix(s, suffix):
251 def remove_suffix(s, suffix):
252 if s.endswith(suffix):
252 if s.endswith(suffix):
253 s = s[:-1 * len(suffix)]
253 s = s[:-1 * len(suffix)]
254 return s
254 return s
255
255
256
256
257 def remove_prefix(s, prefix):
257 def remove_prefix(s, prefix):
258 if s.startswith(prefix):
258 if s.startswith(prefix):
259 s = s[len(prefix):]
259 s = s[len(prefix):]
260 return s
260 return s
261
261
262
262
263 def age(prevdate, show_short_version=False, now=None):
263 def age(prevdate, show_short_version=False, now=None):
264 """
264 """
265 turns a datetime into an age string.
265 turns a datetime into an age string.
266 If show_short_version is True, then it will generate a not so accurate but shorter string,
266 If show_short_version is True, then it will generate a not so accurate but shorter string,
267 example: 2days ago, instead of 2 days and 23 hours ago.
267 example: 2days ago, instead of 2 days and 23 hours ago.
268
268
269 :param prevdate: datetime object
269 :param prevdate: datetime object
270 :param show_short_version: if it should approximate the date and return a shorter string
270 :param show_short_version: if it should approximate the date and return a shorter string
271 :rtype: unicode
271 :rtype: unicode
272 :returns: unicode words describing age
272 :returns: unicode words describing age
273 """
273 """
274 now = now or datetime.datetime.now()
274 now = now or datetime.datetime.now()
275 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
275 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
276 deltas = {}
276 deltas = {}
277 future = False
277 future = False
278
278
279 if prevdate > now:
279 if prevdate > now:
280 now, prevdate = prevdate, now
280 now, prevdate = prevdate, now
281 future = True
281 future = True
282 if future:
282 if future:
283 prevdate = prevdate.replace(microsecond=0)
283 prevdate = prevdate.replace(microsecond=0)
284 # Get date parts deltas
284 # Get date parts deltas
285 from dateutil import relativedelta
285 from dateutil import relativedelta
286 for part in order:
286 for part in order:
287 d = relativedelta.relativedelta(now, prevdate)
287 d = relativedelta.relativedelta(now, prevdate)
288 deltas[part] = getattr(d, part + 's')
288 deltas[part] = getattr(d, part + 's')
289
289
290 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
290 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
291 # not 1 hour, -59 minutes and -59 seconds)
291 # not 1 hour, -59 minutes and -59 seconds)
292 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
292 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
293 part = order[num]
293 part = order[num]
294 carry_part = order[num - 1]
294 carry_part = order[num - 1]
295
295
296 if deltas[part] < 0:
296 if deltas[part] < 0:
297 deltas[part] += length
297 deltas[part] += length
298 deltas[carry_part] -= 1
298 deltas[carry_part] -= 1
299
299
300 # Same thing for days except that the increment depends on the (variable)
300 # Same thing for days except that the increment depends on the (variable)
301 # number of days in the month
301 # number of days in the month
302 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
302 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
303 if deltas['day'] < 0:
303 if deltas['day'] < 0:
304 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
304 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
305 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
305 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
306 deltas['day'] += 29
306 deltas['day'] += 29
307 else:
307 else:
308 deltas['day'] += month_lengths[prevdate.month - 1]
308 deltas['day'] += month_lengths[prevdate.month - 1]
309
309
310 deltas['month'] -= 1
310 deltas['month'] -= 1
311
311
312 if deltas['month'] < 0:
312 if deltas['month'] < 0:
313 deltas['month'] += 12
313 deltas['month'] += 12
314 deltas['year'] -= 1
314 deltas['year'] -= 1
315
315
316 # In short version, we want nicer handling of ages of more than a year
316 # In short version, we want nicer handling of ages of more than a year
317 if show_short_version:
317 if show_short_version:
318 if deltas['year'] == 1:
318 if deltas['year'] == 1:
319 # ages between 1 and 2 years: show as months
319 # ages between 1 and 2 years: show as months
320 deltas['month'] += 12
320 deltas['month'] += 12
321 deltas['year'] = 0
321 deltas['year'] = 0
322 if deltas['year'] >= 2:
322 if deltas['year'] >= 2:
323 # ages 2+ years: round
323 # ages 2+ years: round
324 if deltas['month'] > 6:
324 if deltas['month'] > 6:
325 deltas['year'] += 1
325 deltas['year'] += 1
326 deltas['month'] = 0
326 deltas['month'] = 0
327
327
328 # Format the result
328 # Format the result
329 fmt_funcs = {
329 fmt_funcs = {
330 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
330 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
331 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
331 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
332 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
332 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
333 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
333 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
334 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
334 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
335 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
335 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
336 }
336 }
337
337
338 for i, part in enumerate(order):
338 for i, part in enumerate(order):
339 value = deltas[part]
339 value = deltas[part]
340 if value == 0:
340 if value == 0:
341 continue
341 continue
342
342
343 if i < 5:
343 if i < 5:
344 sub_part = order[i + 1]
344 sub_part = order[i + 1]
345 sub_value = deltas[sub_part]
345 sub_value = deltas[sub_part]
346 else:
346 else:
347 sub_value = 0
347 sub_value = 0
348
348
349 if sub_value == 0 or show_short_version:
349 if sub_value == 0 or show_short_version:
350 if future:
350 if future:
351 return _('in %s') % fmt_funcs[part](value)
351 return _('in %s') % fmt_funcs[part](value)
352 else:
352 else:
353 return _('%s ago') % fmt_funcs[part](value)
353 return _('%s ago') % fmt_funcs[part](value)
354 if future:
354 if future:
355 return _('in %s and %s') % (fmt_funcs[part](value),
355 return _('in %s and %s') % (fmt_funcs[part](value),
356 fmt_funcs[sub_part](sub_value))
356 fmt_funcs[sub_part](sub_value))
357 else:
357 else:
358 return _('%s and %s ago') % (fmt_funcs[part](value),
358 return _('%s and %s ago') % (fmt_funcs[part](value),
359 fmt_funcs[sub_part](sub_value))
359 fmt_funcs[sub_part](sub_value))
360
360
361 return _('just now')
361 return _('just now')
362
362
363
363
364 def uri_filter(uri):
364 def uri_filter(uri):
365 """
365 """
366 Removes user:password from given url string
366 Removes user:password from given url string
367
367
368 :param uri:
368 :param uri:
369 :rtype: unicode
369 :rtype: unicode
370 :returns: filtered list of strings
370 :returns: filtered list of strings
371 """
371 """
372 if not uri:
372 if not uri:
373 return ''
373 return ''
374
374
375 proto = ''
375 proto = ''
376
376
377 for pat in ('https://', 'http://', 'git://'):
377 for pat in ('https://', 'http://', 'git://'):
378 if uri.startswith(pat):
378 if uri.startswith(pat):
379 uri = uri[len(pat):]
379 uri = uri[len(pat):]
380 proto = pat
380 proto = pat
381 break
381 break
382
382
383 # remove passwords and username
383 # remove passwords and username
384 uri = uri[uri.find('@') + 1:]
384 uri = uri[uri.find('@') + 1:]
385
385
386 # get the port
386 # get the port
387 cred_pos = uri.find(':')
387 cred_pos = uri.find(':')
388 if cred_pos == -1:
388 if cred_pos == -1:
389 host, port = uri, None
389 host, port = uri, None
390 else:
390 else:
391 host, port = uri[:cred_pos], uri[cred_pos + 1:]
391 host, port = uri[:cred_pos], uri[cred_pos + 1:]
392
392
393 return filter(None, [proto, host, port])
393 return filter(None, [proto, host, port])
394
394
395
395
396 def credentials_filter(uri):
396 def credentials_filter(uri):
397 """
397 """
398 Returns a url with removed credentials
398 Returns a url with removed credentials
399
399
400 :param uri:
400 :param uri:
401 """
401 """
402
402
403 uri = uri_filter(uri)
403 uri = uri_filter(uri)
404 # check if we have port
404 # check if we have port
405 if len(uri) > 2 and uri[2]:
405 if len(uri) > 2 and uri[2]:
406 uri[2] = ':' + uri[2]
406 uri[2] = ':' + uri[2]
407
407
408 return ''.join(uri)
408 return ''.join(uri)
409
409
410
410
411 def get_clone_url(uri_tmpl, qualified_home_url, repo_name, repo_id, **override):
411 def get_clone_url(uri_tmpl, qualified_home_url, repo_name, repo_id, **override):
412 parsed_url = urlobject.URLObject(qualified_home_url)
412 parsed_url = urlobject.URLObject(qualified_home_url)
413 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
413 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
414 args = {
414 args = {
415 'scheme': parsed_url.scheme,
415 'scheme': parsed_url.scheme,
416 'user': '',
416 'user': '',
417 'netloc': parsed_url.netloc+decoded_path, # path if we use proxy-prefix
417 'netloc': parsed_url.netloc+decoded_path, # path if we use proxy-prefix
418 'prefix': decoded_path,
418 'prefix': decoded_path,
419 'repo': repo_name,
419 'repo': repo_name,
420 'repoid': str(repo_id)
420 'repoid': str(repo_id)
421 }
421 }
422 args.update(override)
422 args.update(override)
423 args['user'] = urllib.quote(safe_str(args['user']))
423 args['user'] = urllib.quote(safe_str(args['user']))
424
424
425 for k, v in args.items():
425 for k, v in args.items():
426 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
426 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
427
427
428 # remove leading @ sign if it's present. Case of empty user
428 # remove leading @ sign if it's present. Case of empty user
429 url_obj = urlobject.URLObject(uri_tmpl)
429 url_obj = urlobject.URLObject(uri_tmpl)
430 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
430 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
431
431
432 return safe_unicode(url)
432 return safe_unicode(url)
433
433
434
434
435 def get_changeset_safe(repo, rev):
435 def get_changeset_safe(repo, rev):
436 """
436 """
437 Safe version of get_changeset if this changeset doesn't exists for a
437 Safe version of get_changeset if this changeset doesn't exists for a
438 repo it returns a Dummy one instead
438 repo it returns a Dummy one instead
439
439
440 :param repo:
440 :param repo:
441 :param rev:
441 :param rev:
442 """
442 """
443 from kallithea.lib.vcs.backends.base import BaseRepository
443 from kallithea.lib.vcs.backends.base import BaseRepository
444 from kallithea.lib.vcs.exceptions import RepositoryError
444 from kallithea.lib.vcs.exceptions import RepositoryError
445 from kallithea.lib.vcs.backends.base import EmptyChangeset
445 from kallithea.lib.vcs.backends.base import EmptyChangeset
446 if not isinstance(repo, BaseRepository):
446 if not isinstance(repo, BaseRepository):
447 raise Exception('You must pass an Repository '
447 raise Exception('You must pass an Repository '
448 'object as first argument got %s', type(repo))
448 'object as first argument got %s', type(repo))
449
449
450 try:
450 try:
451 cs = repo.get_changeset(rev)
451 cs = repo.get_changeset(rev)
452 except (RepositoryError, LookupError):
452 except (RepositoryError, LookupError):
453 cs = EmptyChangeset(requested_revision=rev)
453 cs = EmptyChangeset(requested_revision=rev)
454 return cs
454 return cs
455
455
456
456
457 def datetime_to_time(dt):
457 def datetime_to_time(dt):
458 if dt:
458 if dt:
459 return time.mktime(dt.timetuple())
459 return time.mktime(dt.timetuple())
460
460
461
461
462 def time_to_datetime(tm):
462 def time_to_datetime(tm):
463 if tm:
463 if tm:
464 if isinstance(tm, basestring):
464 if isinstance(tm, basestring):
465 try:
465 try:
466 tm = float(tm)
466 tm = float(tm)
467 except ValueError:
467 except ValueError:
468 return
468 return
469 return datetime.datetime.fromtimestamp(tm)
469 return datetime.datetime.fromtimestamp(tm)
470
470
471
471
472 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
472 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
473 # Check char before @ - it must not look like we are in an email addresses.
473 # Check char before @ - it must not look like we are in an email addresses.
474 # Matching is greedy so we don't have to look beyond the end.
474 # Matching is greedy so we don't have to look beyond the end.
475 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
475 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
476
476
477
477
478 def extract_mentioned_usernames(text):
478 def extract_mentioned_usernames(text):
479 r"""
479 r"""
480 Returns list of (possible) usernames @mentioned in given text.
480 Returns list of (possible) usernames @mentioned in given text.
481
481
482 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
482 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
483 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'hh', 'zz']
483 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'hh', 'zz']
484 """
484 """
485 return MENTIONS_REGEX.findall(text)
485 return MENTIONS_REGEX.findall(text)
486
486
487
487
488 def extract_mentioned_users(text):
488 def extract_mentioned_users(text):
489 """ Returns set of actual database Users @mentioned in given text. """
489 """ Returns set of actual database Users @mentioned in given text. """
490 from kallithea.model.db import User
490 from kallithea.model.db import User
491 result = set()
491 result = set()
492 for name in extract_mentioned_usernames(text):
492 for name in extract_mentioned_usernames(text):
493 user = User.get_by_username(name, case_insensitive=True)
493 user = User.get_by_username(name, case_insensitive=True)
494 if user is not None and not user.is_default_user:
494 if user is not None and not user.is_default_user:
495 result.add(user)
495 result.add(user)
496 return result
496 return result
497
497
498
498
499 class AttributeDict(dict):
499 class AttributeDict(dict):
500 def __getattr__(self, attr):
500 def __getattr__(self, attr):
501 return self.get(attr, None)
501 return self.get(attr, None)
502 __setattr__ = dict.__setitem__
502 __setattr__ = dict.__setitem__
503 __delattr__ = dict.__delitem__
503 __delattr__ = dict.__delitem__
504
504
505
505
506 def fix_PATH(os_=None):
506 def fix_PATH(os_=None):
507 """
507 """
508 Get current active python path, and append it to PATH variable to fix issues
508 Get current active python path, and append it to PATH variable to fix issues
509 of subprocess calls and different python versions
509 of subprocess calls and different python versions
510 """
510 """
511 if os_ is None:
511 if os_ is None:
512 import os
512 import os
513 else:
513 else:
514 os = os_
514 os = os_
515
515
516 cur_path = os.path.split(sys.executable)[0]
516 cur_path = os.path.split(sys.executable)[0]
517 if not os.environ['PATH'].startswith(cur_path):
517 if not os.environ['PATH'].startswith(cur_path):
518 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
518 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
519
519
520
520
521 def obfuscate_url_pw(engine):
521 def obfuscate_url_pw(engine):
522 from sqlalchemy.engine import url as sa_url
522 from sqlalchemy.engine import url as sa_url
523 from sqlalchemy.exc import ArgumentError
523 from sqlalchemy.exc import ArgumentError
524 try:
524 try:
525 _url = sa_url.make_url(engine or '')
525 _url = sa_url.make_url(engine or '')
526 except ArgumentError:
526 except ArgumentError:
527 return engine
527 return engine
528 if _url.password:
528 if _url.password:
529 _url.password = 'XXXXX'
529 _url.password = 'XXXXX'
530 return str(_url)
530 return str(_url)
531
531
532
532
533 def get_server_url(environ):
533 def get_server_url(environ):
534 req = webob.Request(environ)
534 req = webob.Request(environ)
535 return req.host_url + req.script_name
535 return req.host_url + req.script_name
536
536
537
537
538 def _extract_extras(env=None):
538 def _extract_extras(env=None):
539 """
539 """
540 Extracts the Kallithea extras data from os.environ, and wraps it into named
540 Extracts the Kallithea extras data from os.environ, and wraps it into named
541 AttributeDict object
541 AttributeDict object
542 """
542 """
543 if not env:
543 if not env:
544 env = os.environ
544 env = os.environ
545
545
546 try:
546 try:
547 extras = json.loads(env['KALLITHEA_EXTRAS'])
547 extras = json.loads(env['KALLITHEA_EXTRAS'])
548 except KeyError:
548 except KeyError:
549 extras = {}
549 extras = {}
550
550
551 try:
551 try:
552 for k in ['username', 'repository', 'locked_by', 'scm', 'make_lock',
552 for k in ['username', 'repository', 'locked_by', 'scm', 'make_lock',
553 'action', 'ip']:
553 'action', 'ip']:
554 extras[k]
554 extras[k]
555 except KeyError as e:
555 except KeyError as e:
556 raise Exception('Missing key %s in os.environ %s' % (e, extras))
556 raise Exception('Missing key %s in os.environ %s' % (e, extras))
557
557
558 return AttributeDict(extras)
558 return AttributeDict(extras)
559
559
560
560
561 def _set_extras(extras):
561 def _set_extras(extras):
562 # RC_SCM_DATA can probably be removed in the future, but for compatibility now...
562 # RC_SCM_DATA can probably be removed in the future, but for compatibility now...
563 os.environ['KALLITHEA_EXTRAS'] = os.environ['RC_SCM_DATA'] = json.dumps(extras)
563 os.environ['KALLITHEA_EXTRAS'] = os.environ['RC_SCM_DATA'] = json.dumps(extras)
564
564
565
565
566 def get_current_authuser():
566 def get_current_authuser():
567 """
567 """
568 Gets kallithea user from threadlocal tmpl_context variable if it's
568 Gets kallithea user from threadlocal tmpl_context variable if it's
569 defined, else returns None.
569 defined, else returns None.
570 """
570 """
571 from tg import tmpl_context
571 from tg import tmpl_context
572 if hasattr(tmpl_context, 'authuser'):
572 if hasattr(tmpl_context, 'authuser'):
573 return tmpl_context.authuser
573 return tmpl_context.authuser
574
574
575 return None
575 return None
576
576
577
577
578 class OptionalAttr(object):
578 class OptionalAttr(object):
579 """
579 """
580 Special Optional Option that defines other attribute. Example::
580 Special Optional Option that defines other attribute. Example::
581
581
582 def test(apiuser, userid=Optional(OAttr('apiuser')):
582 def test(apiuser, userid=Optional(OAttr('apiuser')):
583 user = Optional.extract(userid)
583 user = Optional.extract(userid)
584 # calls
584 # calls
585
585
586 """
586 """
587
587
588 def __init__(self, attr_name):
588 def __init__(self, attr_name):
589 self.attr_name = attr_name
589 self.attr_name = attr_name
590
590
591 def __repr__(self):
591 def __repr__(self):
592 return '<OptionalAttr:%s>' % self.attr_name
592 return '<OptionalAttr:%s>' % self.attr_name
593
593
594 def __call__(self):
594 def __call__(self):
595 return self
595 return self
596
596
597
597
598 # alias
598 # alias
599 OAttr = OptionalAttr
599 OAttr = OptionalAttr
600
600
601
601
602 class Optional(object):
602 class Optional(object):
603 """
603 """
604 Defines an optional parameter::
604 Defines an optional parameter::
605
605
606 param = param.getval() if isinstance(param, Optional) else param
606 param = param.getval() if isinstance(param, Optional) else param
607 param = param() if isinstance(param, Optional) else param
607 param = param() if isinstance(param, Optional) else param
608
608
609 is equivalent of::
609 is equivalent of::
610
610
611 param = Optional.extract(param)
611 param = Optional.extract(param)
612
612
613 """
613 """
614
614
615 def __init__(self, type_):
615 def __init__(self, type_):
616 self.type_ = type_
616 self.type_ = type_
617
617
618 def __repr__(self):
618 def __repr__(self):
619 return '<Optional:%s>' % self.type_.__repr__()
619 return '<Optional:%s>' % self.type_.__repr__()
620
620
621 def __call__(self):
621 def __call__(self):
622 return self.getval()
622 return self.getval()
623
623
624 def getval(self):
624 def getval(self):
625 """
625 """
626 returns value from this Optional instance
626 returns value from this Optional instance
627 """
627 """
628 if isinstance(self.type_, OAttr):
628 if isinstance(self.type_, OAttr):
629 # use params name
629 # use params name
630 return self.type_.attr_name
630 return self.type_.attr_name
631 return self.type_
631 return self.type_
632
632
633 @classmethod
633 @classmethod
634 def extract(cls, val):
634 def extract(cls, val):
635 """
635 """
636 Extracts value from Optional() instance
636 Extracts value from Optional() instance
637
637
638 :param val:
638 :param val:
639 :return: original value if it's not Optional instance else
639 :return: original value if it's not Optional instance else
640 value of instance
640 value of instance
641 """
641 """
642 if isinstance(val, cls):
642 if isinstance(val, cls):
643 return val.getval()
643 return val.getval()
644 return val
644 return val
645
645
646
646
647 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
647 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
648 return _cleanstringsub('_', safe_str(s)).rstrip('_')
648 return _cleanstringsub('_', safe_str(s)).rstrip('_')
@@ -1,112 +1,112 b''
1 import datetime
1 import datetime
2 import errno
2 import errno
3
3
4 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
4 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
5 from kallithea.lib.vcs.exceptions import RepositoryError
5 from kallithea.lib.vcs.exceptions import RepositoryError
6
6
7 from kallithea.lib.vcs.utils.hgcompat import memfilectx, memctx, hex, tolocal
7 from kallithea.lib.vcs.utils.hgcompat import memfilectx, memctx, hex, tolocal
8
8
9
9
10 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
10 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
11
11
12 def commit(self, message, author, parents=None, branch=None, date=None,
12 def commit(self, message, author, parents=None, branch=None, date=None,
13 **kwargs):
13 **kwargs):
14 """
14 """
15 Performs in-memory commit (doesn't check workdir in any way) and
15 Performs in-memory commit (doesn't check workdir in any way) and
16 returns newly created ``Changeset``. Updates repository's
16 returns newly created ``Changeset``. Updates repository's
17 ``revisions``.
17 ``revisions``.
18
18
19 :param message: message of the commit
19 :param message: message of the commit
20 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
20 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
21 :param parents: single parent or sequence of parents from which commit
21 :param parents: single parent or sequence of parents from which commit
22 would be derived
22 would be derived
23 :param date: ``datetime.datetime`` instance. Defaults to
23 :param date: ``datetime.datetime`` instance. Defaults to
24 ``datetime.datetime.now()``.
24 ``datetime.datetime.now()``.
25 :param branch: branch name, as string. If none given, default backend's
25 :param branch: branch name, as string. If none given, default backend's
26 branch would be used.
26 branch would be used.
27
27
28 :raises ``CommitError``: if any error occurs while committing
28 :raises ``CommitError``: if any error occurs while committing
29 """
29 """
30 self.check_integrity(parents)
30 self.check_integrity(parents)
31
31
32 from .repository import MercurialRepository
32 from .repository import MercurialRepository
33 if not isinstance(message, unicode) or not isinstance(author, unicode):
33 if not isinstance(message, unicode) or not isinstance(author, unicode):
34 raise RepositoryError('Given message and author needs to be '
34 raise RepositoryError('Given message and author needs to be '
35 'an <unicode> instance got %r & %r instead'
35 'an <unicode> instance got %r & %r instead'
36 % (type(message), type(author)))
36 % (type(message), type(author)))
37
37
38 if branch is None:
38 if branch is None:
39 branch = MercurialRepository.DEFAULT_BRANCH_NAME
39 branch = MercurialRepository.DEFAULT_BRANCH_NAME
40 kwargs['branch'] = branch
40 kwargs['branch'] = branch
41
41
42 def filectxfn(_repo, memctx, path):
42 def filectxfn(_repo, memctx, path):
43 """
43 """
44 Marks given path as added/changed/removed in a given _repo. This is
44 Marks given path as added/changed/removed in a given _repo. This is
45 for internal mercurial commit function.
45 for internal mercurial commit function.
46 """
46 """
47
47
48 # check if this path is removed
48 # check if this path is removed
49 if path in (node.path for node in self.removed):
49 if path in (node.path for node in self.removed):
50 return None
50 return None
51
51
52 # check if this path is added
52 # check if this path is added
53 for node in self.added:
53 for node in self.added:
54 if node.path == path:
54 if node.path == path:
55 return memfilectx(_repo, memctx, path=node.path,
55 return memfilectx(_repo, memctx, path=node.path,
56 data=(node.content.encode('utf8')
56 data=(node.content.encode('utf-8')
57 if not node.is_binary else node.content),
57 if not node.is_binary else node.content),
58 islink=False,
58 islink=False,
59 isexec=node.is_executable,
59 isexec=node.is_executable,
60 copied=False)
60 copied=False)
61
61
62 # or changed
62 # or changed
63 for node in self.changed:
63 for node in self.changed:
64 if node.path == path:
64 if node.path == path:
65 return memfilectx(_repo, memctx, path=node.path,
65 return memfilectx(_repo, memctx, path=node.path,
66 data=(node.content.encode('utf8')
66 data=(node.content.encode('utf-8')
67 if not node.is_binary else node.content),
67 if not node.is_binary else node.content),
68 islink=False,
68 islink=False,
69 isexec=node.is_executable,
69 isexec=node.is_executable,
70 copied=False)
70 copied=False)
71
71
72 raise RepositoryError("Given path haven't been marked as added,"
72 raise RepositoryError("Given path haven't been marked as added,"
73 "changed or removed (%s)" % path)
73 "changed or removed (%s)" % path)
74
74
75 parents = [None, None]
75 parents = [None, None]
76 for i, parent in enumerate(self.parents):
76 for i, parent in enumerate(self.parents):
77 if parent is not None:
77 if parent is not None:
78 parents[i] = parent._ctx.node()
78 parents[i] = parent._ctx.node()
79
79
80 if date and isinstance(date, datetime.datetime):
80 if date and isinstance(date, datetime.datetime):
81 date = date.strftime('%a, %d %b %Y %H:%M:%S')
81 date = date.strftime('%a, %d %b %Y %H:%M:%S')
82
82
83 commit_ctx = memctx(repo=self.repository._repo,
83 commit_ctx = memctx(repo=self.repository._repo,
84 parents=parents,
84 parents=parents,
85 text='',
85 text='',
86 files=self.get_paths(),
86 files=self.get_paths(),
87 filectxfn=filectxfn,
87 filectxfn=filectxfn,
88 user=author,
88 user=author,
89 date=date,
89 date=date,
90 extra=kwargs)
90 extra=kwargs)
91
91
92 loc = lambda u: tolocal(u.encode('utf-8'))
92 loc = lambda u: tolocal(u.encode('utf-8'))
93
93
94 # injecting given _repo params
94 # injecting given _repo params
95 commit_ctx._text = loc(message)
95 commit_ctx._text = loc(message)
96 commit_ctx._user = loc(author)
96 commit_ctx._user = loc(author)
97 commit_ctx._date = date
97 commit_ctx._date = date
98
98
99 # TODO: Catch exceptions!
99 # TODO: Catch exceptions!
100 n = self.repository._repo.commitctx(commit_ctx)
100 n = self.repository._repo.commitctx(commit_ctx)
101 # Returns mercurial node
101 # Returns mercurial node
102 self._commit_ctx = commit_ctx # For reference
102 self._commit_ctx = commit_ctx # For reference
103 # Update vcs repository object & recreate mercurial _repo
103 # Update vcs repository object & recreate mercurial _repo
104 # new_ctx = self.repository._repo[node]
104 # new_ctx = self.repository._repo[node]
105 # new_tip = self.repository.get_changeset(new_ctx.hex())
105 # new_tip = self.repository.get_changeset(new_ctx.hex())
106 new_id = hex(n)
106 new_id = hex(n)
107 self.repository.revisions.append(new_id)
107 self.repository.revisions.append(new_id)
108 self._repo = self.repository._get_repo(create=False)
108 self._repo = self.repository._get_repo(create=False)
109 self.repository.branches = self.repository._get_branches()
109 self.repository.branches = self.repository._get_branches()
110 tip = self.repository.get_changeset()
110 tip = self.repository.get_changeset()
111 self.reset()
111 self.reset()
112 return tip
112 return tip
@@ -1,37 +1,37 b''
1 import os
1 import os
2 import tempfile
2 import tempfile
3 from kallithea.lib.vcs.utils import aslist
3 from kallithea.lib.vcs.utils import aslist
4 from kallithea.lib.vcs.utils.paths import get_user_home
4 from kallithea.lib.vcs.utils.paths import get_user_home
5
5
6 abspath = lambda * p: os.path.abspath(os.path.join(*p))
6 abspath = lambda * p: os.path.abspath(os.path.join(*p))
7
7
8 VCSRC_PATH = os.environ.get('VCSRC_PATH')
8 VCSRC_PATH = os.environ.get('VCSRC_PATH')
9
9
10 if not VCSRC_PATH:
10 if not VCSRC_PATH:
11 HOME_ = get_user_home()
11 HOME_ = get_user_home()
12 if not HOME_:
12 if not HOME_:
13 HOME_ = tempfile.gettempdir()
13 HOME_ = tempfile.gettempdir()
14
14
15 VCSRC_PATH = VCSRC_PATH or abspath(HOME_, '.vcsrc')
15 VCSRC_PATH = VCSRC_PATH or abspath(HOME_, '.vcsrc')
16 if os.path.isdir(VCSRC_PATH):
16 if os.path.isdir(VCSRC_PATH):
17 VCSRC_PATH = os.path.join(VCSRC_PATH, '__init__.py')
17 VCSRC_PATH = os.path.join(VCSRC_PATH, '__init__.py')
18
18
19 # list of default encoding used in safe_unicode/safe_str methods
19 # list of default encoding used in safe_unicode/safe_str methods
20 DEFAULT_ENCODINGS = aslist('utf8')
20 DEFAULT_ENCODINGS = aslist('utf-8')
21
21
22 # path to git executable run by run_git_command function
22 # path to git executable run by run_git_command function
23 GIT_EXECUTABLE_PATH = 'git'
23 GIT_EXECUTABLE_PATH = 'git'
24 # can be also --branches --tags
24 # can be also --branches --tags
25 GIT_REV_FILTER = '--all'
25 GIT_REV_FILTER = '--all'
26
26
27 BACKENDS = {
27 BACKENDS = {
28 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
28 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
29 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
29 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
30 }
30 }
31
31
32 ARCHIVE_SPECS = {
32 ARCHIVE_SPECS = {
33 'tar': ('application/x-tar', '.tar'),
33 'tar': ('application/x-tar', '.tar'),
34 'tbz2': ('application/x-bzip2', '.tar.bz2'),
34 'tbz2': ('application/x-bzip2', '.tar.bz2'),
35 'tgz': ('application/x-gzip', '.tar.gz'),
35 'tgz': ('application/x-gzip', '.tar.gz'),
36 'zip': ('application/zip', '.zip'),
36 'zip': ('application/zip', '.zip'),
37 }
37 }
@@ -1,394 +1,394 b''
1 # encoding: utf8
1 # encoding: utf-8
2
2
3 import time
3 import time
4 import datetime
4 import datetime
5
5
6 import pytest
6 import pytest
7
7
8 from kallithea.lib import vcs
8 from kallithea.lib import vcs
9
9
10 from kallithea.lib.vcs.backends.base import BaseChangeset
10 from kallithea.lib.vcs.backends.base import BaseChangeset
11 from kallithea.lib.vcs.nodes import (
11 from kallithea.lib.vcs.nodes import (
12 FileNode, AddedFileNodesGenerator,
12 FileNode, AddedFileNodesGenerator,
13 ChangedFileNodesGenerator, RemovedFileNodesGenerator
13 ChangedFileNodesGenerator, RemovedFileNodesGenerator
14 )
14 )
15 from kallithea.lib.vcs.exceptions import (
15 from kallithea.lib.vcs.exceptions import (
16 BranchDoesNotExistError, ChangesetDoesNotExistError,
16 BranchDoesNotExistError, ChangesetDoesNotExistError,
17 RepositoryError, EmptyRepositoryError
17 RepositoryError, EmptyRepositoryError
18 )
18 )
19
19
20 from kallithea.tests.vcs.base import _BackendTestMixin
20 from kallithea.tests.vcs.base import _BackendTestMixin
21 from kallithea.tests.vcs.conf import get_new_dir
21 from kallithea.tests.vcs.conf import get_new_dir
22
22
23
23
24 class TestBaseChangeset(object):
24 class TestBaseChangeset(object):
25
25
26 def test_as_dict(self):
26 def test_as_dict(self):
27 changeset = BaseChangeset()
27 changeset = BaseChangeset()
28 changeset.id = 'ID'
28 changeset.id = 'ID'
29 changeset.raw_id = 'RAW_ID'
29 changeset.raw_id = 'RAW_ID'
30 changeset.short_id = 'SHORT_ID'
30 changeset.short_id = 'SHORT_ID'
31 changeset.revision = 1009
31 changeset.revision = 1009
32 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
32 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
33 changeset.message = 'Message of a commit'
33 changeset.message = 'Message of a commit'
34 changeset.author = 'Joe Doe <joe.doe@example.com>'
34 changeset.author = 'Joe Doe <joe.doe@example.com>'
35 changeset.added = [FileNode('foo/bar/baz'), FileNode(u'foobar'), FileNode(u'blΓ₯bΓ¦rgrΓΈd')]
35 changeset.added = [FileNode('foo/bar/baz'), FileNode(u'foobar'), FileNode(u'blΓ₯bΓ¦rgrΓΈd')]
36 changeset.changed = []
36 changeset.changed = []
37 changeset.removed = []
37 changeset.removed = []
38 assert changeset.as_dict() == {
38 assert changeset.as_dict() == {
39 'id': 'ID',
39 'id': 'ID',
40 'raw_id': 'RAW_ID',
40 'raw_id': 'RAW_ID',
41 'short_id': 'SHORT_ID',
41 'short_id': 'SHORT_ID',
42 'revision': 1009,
42 'revision': 1009,
43 'date': datetime.datetime(2011, 1, 30, 1, 45),
43 'date': datetime.datetime(2011, 1, 30, 1, 45),
44 'message': 'Message of a commit',
44 'message': 'Message of a commit',
45 'author': {
45 'author': {
46 'name': 'Joe Doe',
46 'name': 'Joe Doe',
47 'email': 'joe.doe@example.com',
47 'email': 'joe.doe@example.com',
48 },
48 },
49 'added': ['foo/bar/baz', 'foobar', u'bl\xe5b\xe6rgr\xf8d'],
49 'added': ['foo/bar/baz', 'foobar', u'bl\xe5b\xe6rgr\xf8d'],
50 'changed': [],
50 'changed': [],
51 'removed': [],
51 'removed': [],
52 }
52 }
53
53
54
54
55 class _ChangesetsWithCommitsTestCaseixin(_BackendTestMixin):
55 class _ChangesetsWithCommitsTestCaseixin(_BackendTestMixin):
56
56
57 @classmethod
57 @classmethod
58 def _get_commits(cls):
58 def _get_commits(cls):
59 start_date = datetime.datetime(2010, 1, 1, 20)
59 start_date = datetime.datetime(2010, 1, 1, 20)
60 for x in xrange(5):
60 for x in xrange(5):
61 yield {
61 yield {
62 'message': 'Commit %d' % x,
62 'message': 'Commit %d' % x,
63 'author': 'Joe Doe <joe.doe@example.com>',
63 'author': 'Joe Doe <joe.doe@example.com>',
64 'date': start_date + datetime.timedelta(hours=12 * x),
64 'date': start_date + datetime.timedelta(hours=12 * x),
65 'added': [
65 'added': [
66 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
66 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
67 ],
67 ],
68 }
68 }
69
69
70 def test_new_branch(self):
70 def test_new_branch(self):
71 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
71 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
72 content='Documentation\n'))
72 content='Documentation\n'))
73 foobar_tip = self.imc.commit(
73 foobar_tip = self.imc.commit(
74 message=u'New branch: foobar',
74 message=u'New branch: foobar',
75 author=u'joe',
75 author=u'joe',
76 branch='foobar',
76 branch='foobar',
77 )
77 )
78 assert 'foobar' in self.repo.branches
78 assert 'foobar' in self.repo.branches
79 assert foobar_tip.branch == 'foobar'
79 assert foobar_tip.branch == 'foobar'
80 assert foobar_tip.branches == ['foobar']
80 assert foobar_tip.branches == ['foobar']
81 # 'foobar' should be the only branch that contains the new commit
81 # 'foobar' should be the only branch that contains the new commit
82 branch_tips = self.repo.branches.values()
82 branch_tips = self.repo.branches.values()
83 assert branch_tips.count(str(foobar_tip.raw_id)) == 1
83 assert branch_tips.count(str(foobar_tip.raw_id)) == 1
84
84
85 def test_new_head_in_default_branch(self):
85 def test_new_head_in_default_branch(self):
86 tip = self.repo.get_changeset()
86 tip = self.repo.get_changeset()
87 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
87 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
88 content='Documentation\n'))
88 content='Documentation\n'))
89 foobar_tip = self.imc.commit(
89 foobar_tip = self.imc.commit(
90 message=u'New branch: foobar',
90 message=u'New branch: foobar',
91 author=u'joe',
91 author=u'joe',
92 branch='foobar',
92 branch='foobar',
93 parents=[tip],
93 parents=[tip],
94 )
94 )
95 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
95 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
96 content='Documentation\nand more...\n'))
96 content='Documentation\nand more...\n'))
97 newtip = self.imc.commit(
97 newtip = self.imc.commit(
98 message=u'At default branch',
98 message=u'At default branch',
99 author=u'joe',
99 author=u'joe',
100 branch=foobar_tip.branch,
100 branch=foobar_tip.branch,
101 parents=[foobar_tip],
101 parents=[foobar_tip],
102 )
102 )
103
103
104 newest_tip = self.imc.commit(
104 newest_tip = self.imc.commit(
105 message=u'Merged with %s' % foobar_tip.raw_id,
105 message=u'Merged with %s' % foobar_tip.raw_id,
106 author=u'joe',
106 author=u'joe',
107 branch=self.backend_class.DEFAULT_BRANCH_NAME,
107 branch=self.backend_class.DEFAULT_BRANCH_NAME,
108 parents=[newtip, foobar_tip],
108 parents=[newtip, foobar_tip],
109 )
109 )
110
110
111 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
111 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
112 assert newest_tip.branches == [self.backend_class.DEFAULT_BRANCH_NAME]
112 assert newest_tip.branches == [self.backend_class.DEFAULT_BRANCH_NAME]
113
113
114 def test_get_changesets_respects_branch_name(self):
114 def test_get_changesets_respects_branch_name(self):
115 tip = self.repo.get_changeset()
115 tip = self.repo.get_changeset()
116 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
116 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
117 content='Documentation\n'))
117 content='Documentation\n'))
118 doc_changeset = self.imc.commit(
118 doc_changeset = self.imc.commit(
119 message=u'New branch: docs',
119 message=u'New branch: docs',
120 author=u'joe',
120 author=u'joe',
121 branch='docs',
121 branch='docs',
122 )
122 )
123 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
123 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
124 self.imc.commit(
124 self.imc.commit(
125 message=u'Back in default branch',
125 message=u'Back in default branch',
126 author=u'joe',
126 author=u'joe',
127 parents=[tip],
127 parents=[tip],
128 )
128 )
129 default_branch_changesets = self.repo.get_changesets(
129 default_branch_changesets = self.repo.get_changesets(
130 branch_name=self.repo.DEFAULT_BRANCH_NAME)
130 branch_name=self.repo.DEFAULT_BRANCH_NAME)
131 assert doc_changeset not in default_branch_changesets
131 assert doc_changeset not in default_branch_changesets
132
132
133 def test_get_changeset_by_branch(self):
133 def test_get_changeset_by_branch(self):
134 for branch, sha in self.repo.branches.iteritems():
134 for branch, sha in self.repo.branches.iteritems():
135 assert sha == self.repo.get_changeset(branch).raw_id
135 assert sha == self.repo.get_changeset(branch).raw_id
136
136
137 def test_get_changeset_by_tag(self):
137 def test_get_changeset_by_tag(self):
138 for tag, sha in self.repo.tags.iteritems():
138 for tag, sha in self.repo.tags.iteritems():
139 assert sha == self.repo.get_changeset(tag).raw_id
139 assert sha == self.repo.get_changeset(tag).raw_id
140
140
141 def test_get_changeset_parents(self):
141 def test_get_changeset_parents(self):
142 for test_rev in [1, 2, 3]:
142 for test_rev in [1, 2, 3]:
143 sha = self.repo.get_changeset(test_rev-1)
143 sha = self.repo.get_changeset(test_rev-1)
144 assert [sha] == self.repo.get_changeset(test_rev).parents
144 assert [sha] == self.repo.get_changeset(test_rev).parents
145
145
146 def test_get_changeset_children(self):
146 def test_get_changeset_children(self):
147 for test_rev in [1, 2, 3]:
147 for test_rev in [1, 2, 3]:
148 sha = self.repo.get_changeset(test_rev+1)
148 sha = self.repo.get_changeset(test_rev+1)
149 assert [sha] == self.repo.get_changeset(test_rev).children
149 assert [sha] == self.repo.get_changeset(test_rev).children
150
150
151
151
152 class _ChangesetsTestCaseMixin(_BackendTestMixin):
152 class _ChangesetsTestCaseMixin(_BackendTestMixin):
153 recreate_repo_per_test = False
153 recreate_repo_per_test = False
154
154
155 @classmethod
155 @classmethod
156 def _get_commits(cls):
156 def _get_commits(cls):
157 start_date = datetime.datetime(2010, 1, 1, 20)
157 start_date = datetime.datetime(2010, 1, 1, 20)
158 for x in xrange(5):
158 for x in xrange(5):
159 yield {
159 yield {
160 'message': u'Commit %d' % x,
160 'message': u'Commit %d' % x,
161 'author': u'Joe Doe <joe.doe@example.com>',
161 'author': u'Joe Doe <joe.doe@example.com>',
162 'date': start_date + datetime.timedelta(hours=12 * x),
162 'date': start_date + datetime.timedelta(hours=12 * x),
163 'added': [
163 'added': [
164 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
164 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
165 ],
165 ],
166 }
166 }
167
167
168 def test_simple(self):
168 def test_simple(self):
169 tip = self.repo.get_changeset()
169 tip = self.repo.get_changeset()
170 assert tip.date == datetime.datetime(2010, 1, 3, 20)
170 assert tip.date == datetime.datetime(2010, 1, 3, 20)
171
171
172 def test_get_changesets_is_ordered_by_date(self):
172 def test_get_changesets_is_ordered_by_date(self):
173 changesets = list(self.repo.get_changesets())
173 changesets = list(self.repo.get_changesets())
174 ordered_by_date = sorted(changesets,
174 ordered_by_date = sorted(changesets,
175 key=lambda cs: cs.date)
175 key=lambda cs: cs.date)
176
176
177 assert changesets == ordered_by_date
177 assert changesets == ordered_by_date
178
178
179 def test_get_changesets_respects_start(self):
179 def test_get_changesets_respects_start(self):
180 second_id = self.repo.revisions[1]
180 second_id = self.repo.revisions[1]
181 changesets = list(self.repo.get_changesets(start=second_id))
181 changesets = list(self.repo.get_changesets(start=second_id))
182 assert len(changesets) == 4
182 assert len(changesets) == 4
183
183
184 def test_get_changesets_numerical_id_respects_start(self):
184 def test_get_changesets_numerical_id_respects_start(self):
185 second_id = 1
185 second_id = 1
186 changesets = list(self.repo.get_changesets(start=second_id))
186 changesets = list(self.repo.get_changesets(start=second_id))
187 assert len(changesets) == 4
187 assert len(changesets) == 4
188
188
189 def test_get_changesets_includes_start_changeset(self):
189 def test_get_changesets_includes_start_changeset(self):
190 second_id = self.repo.revisions[1]
190 second_id = self.repo.revisions[1]
191 changesets = list(self.repo.get_changesets(start=second_id))
191 changesets = list(self.repo.get_changesets(start=second_id))
192 assert changesets[0].raw_id == second_id
192 assert changesets[0].raw_id == second_id
193
193
194 def test_get_changesets_respects_end(self):
194 def test_get_changesets_respects_end(self):
195 second_id = self.repo.revisions[1]
195 second_id = self.repo.revisions[1]
196 changesets = list(self.repo.get_changesets(end=second_id))
196 changesets = list(self.repo.get_changesets(end=second_id))
197 assert changesets[-1].raw_id == second_id
197 assert changesets[-1].raw_id == second_id
198 assert len(changesets) == 2
198 assert len(changesets) == 2
199
199
200 def test_get_changesets_numerical_id_respects_end(self):
200 def test_get_changesets_numerical_id_respects_end(self):
201 second_id = 1
201 second_id = 1
202 changesets = list(self.repo.get_changesets(end=second_id))
202 changesets = list(self.repo.get_changesets(end=second_id))
203 assert changesets.index(changesets[-1]) == second_id
203 assert changesets.index(changesets[-1]) == second_id
204 assert len(changesets) == 2
204 assert len(changesets) == 2
205
205
206 def test_get_changesets_respects_both_start_and_end(self):
206 def test_get_changesets_respects_both_start_and_end(self):
207 second_id = self.repo.revisions[1]
207 second_id = self.repo.revisions[1]
208 third_id = self.repo.revisions[2]
208 third_id = self.repo.revisions[2]
209 changesets = list(self.repo.get_changesets(start=second_id,
209 changesets = list(self.repo.get_changesets(start=second_id,
210 end=third_id))
210 end=third_id))
211 assert len(changesets) == 2
211 assert len(changesets) == 2
212
212
213 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
213 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
214 changesets = list(self.repo.get_changesets(start=2, end=3))
214 changesets = list(self.repo.get_changesets(start=2, end=3))
215 assert len(changesets) == 2
215 assert len(changesets) == 2
216
216
217 def test_get_changesets_on_empty_repo_raises_EmptyRepository_error(self):
217 def test_get_changesets_on_empty_repo_raises_EmptyRepository_error(self):
218 repo = self.setup_empty_repo(self.backend_class)
218 repo = self.setup_empty_repo(self.backend_class)
219 with pytest.raises(EmptyRepositoryError):
219 with pytest.raises(EmptyRepositoryError):
220 list(repo.get_changesets(start='foobar'))
220 list(repo.get_changesets(start='foobar'))
221
221
222 def test_get_changesets_includes_end_changeset(self):
222 def test_get_changesets_includes_end_changeset(self):
223 second_id = self.repo.revisions[1]
223 second_id = self.repo.revisions[1]
224 changesets = list(self.repo.get_changesets(end=second_id))
224 changesets = list(self.repo.get_changesets(end=second_id))
225 assert changesets[-1].raw_id == second_id
225 assert changesets[-1].raw_id == second_id
226
226
227 def test_get_changesets_respects_start_date(self):
227 def test_get_changesets_respects_start_date(self):
228 start_date = datetime.datetime(2010, 2, 1)
228 start_date = datetime.datetime(2010, 2, 1)
229 for cs in self.repo.get_changesets(start_date=start_date):
229 for cs in self.repo.get_changesets(start_date=start_date):
230 assert cs.date >= start_date
230 assert cs.date >= start_date
231
231
232 def test_get_changesets_respects_end_date(self):
232 def test_get_changesets_respects_end_date(self):
233 start_date = datetime.datetime(2010, 1, 1)
233 start_date = datetime.datetime(2010, 1, 1)
234 end_date = datetime.datetime(2010, 2, 1)
234 end_date = datetime.datetime(2010, 2, 1)
235 for cs in self.repo.get_changesets(start_date=start_date,
235 for cs in self.repo.get_changesets(start_date=start_date,
236 end_date=end_date):
236 end_date=end_date):
237 assert cs.date >= start_date
237 assert cs.date >= start_date
238 assert cs.date <= end_date
238 assert cs.date <= end_date
239
239
240 def test_get_changesets_respects_start_date_and_end_date(self):
240 def test_get_changesets_respects_start_date_and_end_date(self):
241 end_date = datetime.datetime(2010, 2, 1)
241 end_date = datetime.datetime(2010, 2, 1)
242 for cs in self.repo.get_changesets(end_date=end_date):
242 for cs in self.repo.get_changesets(end_date=end_date):
243 assert cs.date <= end_date
243 assert cs.date <= end_date
244
244
245 def test_get_changesets_respects_reverse(self):
245 def test_get_changesets_respects_reverse(self):
246 changesets_id_list = [cs.raw_id for cs in
246 changesets_id_list = [cs.raw_id for cs in
247 self.repo.get_changesets(reverse=True)]
247 self.repo.get_changesets(reverse=True)]
248 assert changesets_id_list == list(reversed(self.repo.revisions))
248 assert changesets_id_list == list(reversed(self.repo.revisions))
249
249
250 def test_get_filenodes_generator(self):
250 def test_get_filenodes_generator(self):
251 tip = self.repo.get_changeset()
251 tip = self.repo.get_changeset()
252 filepaths = [node.path for node in tip.get_filenodes_generator()]
252 filepaths = [node.path for node in tip.get_filenodes_generator()]
253 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
253 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
254
254
255 def test_size(self):
255 def test_size(self):
256 tip = self.repo.get_changeset()
256 tip = self.repo.get_changeset()
257 size = 5 * len('Foobar N') # Size of 5 files
257 size = 5 * len('Foobar N') # Size of 5 files
258 assert tip.size == size
258 assert tip.size == size
259
259
260 def test_author(self):
260 def test_author(self):
261 tip = self.repo.get_changeset()
261 tip = self.repo.get_changeset()
262 assert tip.author == u'Joe Doe <joe.doe@example.com>'
262 assert tip.author == u'Joe Doe <joe.doe@example.com>'
263
263
264 def test_author_name(self):
264 def test_author_name(self):
265 tip = self.repo.get_changeset()
265 tip = self.repo.get_changeset()
266 assert tip.author_name == u'Joe Doe'
266 assert tip.author_name == u'Joe Doe'
267
267
268 def test_author_email(self):
268 def test_author_email(self):
269 tip = self.repo.get_changeset()
269 tip = self.repo.get_changeset()
270 assert tip.author_email == u'joe.doe@example.com'
270 assert tip.author_email == u'joe.doe@example.com'
271
271
272 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
272 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
273 with pytest.raises(ChangesetDoesNotExistError):
273 with pytest.raises(ChangesetDoesNotExistError):
274 list(self.repo.get_changesets(start='foobar'))
274 list(self.repo.get_changesets(start='foobar'))
275
275
276 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
276 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
277 with pytest.raises(ChangesetDoesNotExistError):
277 with pytest.raises(ChangesetDoesNotExistError):
278 list(self.repo.get_changesets(end='foobar'))
278 list(self.repo.get_changesets(end='foobar'))
279
279
280 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
280 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
281 with pytest.raises(BranchDoesNotExistError):
281 with pytest.raises(BranchDoesNotExistError):
282 list(self.repo.get_changesets(branch_name='foobar'))
282 list(self.repo.get_changesets(branch_name='foobar'))
283
283
284 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
284 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
285 start = self.repo.revisions[-1]
285 start = self.repo.revisions[-1]
286 end = self.repo.revisions[0]
286 end = self.repo.revisions[0]
287 with pytest.raises(RepositoryError):
287 with pytest.raises(RepositoryError):
288 list(self.repo.get_changesets(start=start, end=end))
288 list(self.repo.get_changesets(start=start, end=end))
289
289
290 def test_get_changesets_numerical_id_reversed(self):
290 def test_get_changesets_numerical_id_reversed(self):
291 with pytest.raises(RepositoryError):
291 with pytest.raises(RepositoryError):
292 [x for x in self.repo.get_changesets(start=3, end=2)]
292 [x for x in self.repo.get_changesets(start=3, end=2)]
293
293
294 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
294 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
295 with pytest.raises(RepositoryError):
295 with pytest.raises(RepositoryError):
296 last = len(self.repo.revisions)
296 last = len(self.repo.revisions)
297 list(self.repo.get_changesets(start=last-1, end=last-2))
297 list(self.repo.get_changesets(start=last-1, end=last-2))
298
298
299 def test_get_changesets_numerical_id_last_zero_error(self):
299 def test_get_changesets_numerical_id_last_zero_error(self):
300 with pytest.raises(RepositoryError):
300 with pytest.raises(RepositoryError):
301 last = len(self.repo.revisions)
301 last = len(self.repo.revisions)
302 list(self.repo.get_changesets(start=last-1, end=0))
302 list(self.repo.get_changesets(start=last-1, end=0))
303
303
304
304
305 class _ChangesetsChangesTestCaseMixin(_BackendTestMixin):
305 class _ChangesetsChangesTestCaseMixin(_BackendTestMixin):
306 recreate_repo_per_test = False
306 recreate_repo_per_test = False
307
307
308 @classmethod
308 @classmethod
309 def _get_commits(cls):
309 def _get_commits(cls):
310 return [
310 return [
311 {
311 {
312 'message': u'Initial',
312 'message': u'Initial',
313 'author': u'Joe Doe <joe.doe@example.com>',
313 'author': u'Joe Doe <joe.doe@example.com>',
314 'date': datetime.datetime(2010, 1, 1, 20),
314 'date': datetime.datetime(2010, 1, 1, 20),
315 'added': [
315 'added': [
316 FileNode('foo/bar', content='foo'),
316 FileNode('foo/bar', content='foo'),
317 FileNode('foo/baΕ‚', content='foo'),
317 FileNode('foo/baΕ‚', content='foo'),
318 FileNode('foobar', content='foo'),
318 FileNode('foobar', content='foo'),
319 FileNode('qwe', content='foo'),
319 FileNode('qwe', content='foo'),
320 ],
320 ],
321 },
321 },
322 {
322 {
323 'message': u'Massive changes',
323 'message': u'Massive changes',
324 'author': u'Joe Doe <joe.doe@example.com>',
324 'author': u'Joe Doe <joe.doe@example.com>',
325 'date': datetime.datetime(2010, 1, 1, 22),
325 'date': datetime.datetime(2010, 1, 1, 22),
326 'added': [FileNode('fallout', content='War never changes')],
326 'added': [FileNode('fallout', content='War never changes')],
327 'changed': [
327 'changed': [
328 FileNode('foo/bar', content='baz'),
328 FileNode('foo/bar', content='baz'),
329 FileNode('foobar', content='baz'),
329 FileNode('foobar', content='baz'),
330 ],
330 ],
331 'removed': [FileNode('qwe')],
331 'removed': [FileNode('qwe')],
332 },
332 },
333 ]
333 ]
334
334
335 def test_initial_commit(self):
335 def test_initial_commit(self):
336 changeset = self.repo.get_changeset(0)
336 changeset = self.repo.get_changeset(0)
337 assert sorted(list(changeset.added)) == sorted([
337 assert sorted(list(changeset.added)) == sorted([
338 changeset.get_node('foo/bar'),
338 changeset.get_node('foo/bar'),
339 changeset.get_node('foo/baΕ‚'),
339 changeset.get_node('foo/baΕ‚'),
340 changeset.get_node('foobar'),
340 changeset.get_node('foobar'),
341 changeset.get_node('qwe'),
341 changeset.get_node('qwe'),
342 ])
342 ])
343 assert list(changeset.changed) == []
343 assert list(changeset.changed) == []
344 assert list(changeset.removed) == []
344 assert list(changeset.removed) == []
345 assert u'foo/ba\u0142' in changeset.as_dict()['added']
345 assert u'foo/ba\u0142' in changeset.as_dict()['added']
346 assert u'foo/ba\u0142' in changeset.__json__(with_file_list=True)['added']
346 assert u'foo/ba\u0142' in changeset.__json__(with_file_list=True)['added']
347
347
348 def test_head_added(self):
348 def test_head_added(self):
349 changeset = self.repo.get_changeset()
349 changeset = self.repo.get_changeset()
350 assert isinstance(changeset.added, AddedFileNodesGenerator)
350 assert isinstance(changeset.added, AddedFileNodesGenerator)
351 assert list(changeset.added) == [
351 assert list(changeset.added) == [
352 changeset.get_node('fallout'),
352 changeset.get_node('fallout'),
353 ]
353 ]
354 assert isinstance(changeset.changed, ChangedFileNodesGenerator)
354 assert isinstance(changeset.changed, ChangedFileNodesGenerator)
355 assert list(changeset.changed) == [
355 assert list(changeset.changed) == [
356 changeset.get_node('foo/bar'),
356 changeset.get_node('foo/bar'),
357 changeset.get_node('foobar'),
357 changeset.get_node('foobar'),
358 ]
358 ]
359 assert isinstance(changeset.removed, RemovedFileNodesGenerator)
359 assert isinstance(changeset.removed, RemovedFileNodesGenerator)
360 assert len(changeset.removed) == 1
360 assert len(changeset.removed) == 1
361 assert list(changeset.removed)[0].path == 'qwe'
361 assert list(changeset.removed)[0].path == 'qwe'
362
362
363 def test_get_filemode(self):
363 def test_get_filemode(self):
364 changeset = self.repo.get_changeset()
364 changeset = self.repo.get_changeset()
365 assert 33188 == changeset.get_file_mode('foo/bar')
365 assert 33188 == changeset.get_file_mode('foo/bar')
366
366
367 def test_get_filemode_non_ascii(self):
367 def test_get_filemode_non_ascii(self):
368 changeset = self.repo.get_changeset()
368 changeset = self.repo.get_changeset()
369 assert 33188 == changeset.get_file_mode('foo/baΕ‚')
369 assert 33188 == changeset.get_file_mode('foo/baΕ‚')
370 assert 33188 == changeset.get_file_mode(u'foo/baΕ‚')
370 assert 33188 == changeset.get_file_mode(u'foo/baΕ‚')
371
371
372
372
373 class TestGitChangesetsWithCommits(_ChangesetsWithCommitsTestCaseixin):
373 class TestGitChangesetsWithCommits(_ChangesetsWithCommitsTestCaseixin):
374 backend_alias = 'git'
374 backend_alias = 'git'
375
375
376
376
377 class TestGitChangesets(_ChangesetsTestCaseMixin):
377 class TestGitChangesets(_ChangesetsTestCaseMixin):
378 backend_alias = 'git'
378 backend_alias = 'git'
379
379
380
380
381 class TestGitChangesetsChanges(_ChangesetsChangesTestCaseMixin):
381 class TestGitChangesetsChanges(_ChangesetsChangesTestCaseMixin):
382 backend_alias = 'git'
382 backend_alias = 'git'
383
383
384
384
385 class TestHgChangesetsWithCommits(_ChangesetsWithCommitsTestCaseixin):
385 class TestHgChangesetsWithCommits(_ChangesetsWithCommitsTestCaseixin):
386 backend_alias = 'hg'
386 backend_alias = 'hg'
387
387
388
388
389 class TestHgChangesets(_ChangesetsTestCaseMixin):
389 class TestHgChangesets(_ChangesetsTestCaseMixin):
390 backend_alias = 'hg'
390 backend_alias = 'hg'
391
391
392
392
393 class TestHgChangesetsChanges(_ChangesetsChangesTestCaseMixin):
393 class TestHgChangesetsChanges(_ChangesetsChangesTestCaseMixin):
394 backend_alias = 'hg'
394 backend_alias = 'hg'
@@ -1,41 +1,41 b''
1 # encoding: utf8
1 # encoding: utf-8
2
2
3 import datetime
3 import datetime
4
4
5 from kallithea.lib.vcs.nodes import FileNode
5 from kallithea.lib.vcs.nodes import FileNode
6 from kallithea.tests.vcs.base import _BackendTestMixin
6 from kallithea.tests.vcs.base import _BackendTestMixin
7
7
8
8
9 class FileNodeUnicodePathTestsMixin(_BackendTestMixin):
9 class FileNodeUnicodePathTestsMixin(_BackendTestMixin):
10
10
11 fname = 'Δ…Ε›Γ°Δ…Δ™Ε‚Δ…Δ‡.txt'
11 fname = 'Δ…Ε›Γ°Δ…Δ™Ε‚Δ…Δ‡.txt'
12 ufname = (fname).decode('utf-8')
12 ufname = (fname).decode('utf-8')
13
13
14 @classmethod
14 @classmethod
15 def _get_commits(cls):
15 def _get_commits(cls):
16 cls.nodes = [
16 cls.nodes = [
17 FileNode(cls.fname, content='Foobar'),
17 FileNode(cls.fname, content='Foobar'),
18 ]
18 ]
19
19
20 commits = [
20 commits = [
21 {
21 {
22 'message': 'Initial commit',
22 'message': 'Initial commit',
23 'author': 'Joe Doe <joe.doe@example.com>',
23 'author': 'Joe Doe <joe.doe@example.com>',
24 'date': datetime.datetime(2010, 1, 1, 20),
24 'date': datetime.datetime(2010, 1, 1, 20),
25 'added': cls.nodes,
25 'added': cls.nodes,
26 },
26 },
27 ]
27 ]
28 return commits
28 return commits
29
29
30 def test_filenode_path(self):
30 def test_filenode_path(self):
31 node = self.tip.get_node(self.fname)
31 node = self.tip.get_node(self.fname)
32 unode = self.tip.get_node(self.ufname)
32 unode = self.tip.get_node(self.ufname)
33 assert node == unode
33 assert node == unode
34
34
35
35
36 class TestGitFileNodeUnicodePath(FileNodeUnicodePathTestsMixin):
36 class TestGitFileNodeUnicodePath(FileNodeUnicodePathTestsMixin):
37 backend_alias = 'git'
37 backend_alias = 'git'
38
38
39
39
40 class TestHgFileNodeUnicodePath(FileNodeUnicodePathTestsMixin):
40 class TestHgFileNodeUnicodePath(FileNodeUnicodePathTestsMixin):
41 backend_alias = 'hg'
41 backend_alias = 'hg'
@@ -1,342 +1,342 b''
1 # encoding: utf8
1 # encoding: utf-8
2 """
2 """
3 Tests so called "in memory changesets" commit API of vcs.
3 Tests so called "in memory changesets" commit API of vcs.
4 """
4 """
5
5
6 import time
6 import time
7 import datetime
7 import datetime
8
8
9 import pytest
9 import pytest
10
10
11 from kallithea.lib import vcs
11 from kallithea.lib import vcs
12 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
12 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
13 from kallithea.lib.vcs.exceptions import NodeAlreadyAddedError
13 from kallithea.lib.vcs.exceptions import NodeAlreadyAddedError
14 from kallithea.lib.vcs.exceptions import NodeAlreadyExistsError
14 from kallithea.lib.vcs.exceptions import NodeAlreadyExistsError
15 from kallithea.lib.vcs.exceptions import NodeAlreadyRemovedError
15 from kallithea.lib.vcs.exceptions import NodeAlreadyRemovedError
16 from kallithea.lib.vcs.exceptions import NodeAlreadyChangedError
16 from kallithea.lib.vcs.exceptions import NodeAlreadyChangedError
17 from kallithea.lib.vcs.exceptions import NodeDoesNotExistError
17 from kallithea.lib.vcs.exceptions import NodeDoesNotExistError
18 from kallithea.lib.vcs.exceptions import NodeNotChangedError
18 from kallithea.lib.vcs.exceptions import NodeNotChangedError
19 from kallithea.lib.vcs.nodes import DirNode
19 from kallithea.lib.vcs.nodes import DirNode
20 from kallithea.lib.vcs.nodes import FileNode
20 from kallithea.lib.vcs.nodes import FileNode
21 from kallithea.lib.vcs.utils import safe_unicode
21 from kallithea.lib.vcs.utils import safe_unicode
22
22
23 from kallithea.tests.vcs.base import _BackendTestMixin
23 from kallithea.tests.vcs.base import _BackendTestMixin
24
24
25
25
26 class InMemoryChangesetTestMixin(_BackendTestMixin):
26 class InMemoryChangesetTestMixin(_BackendTestMixin):
27
27
28 @classmethod
28 @classmethod
29 def _get_commits(cls):
29 def _get_commits(cls):
30 # Note: this is slightly different than the regular _get_commits methods
30 # Note: this is slightly different than the regular _get_commits methods
31 # as we don't actually return any commits. The creation of commits is
31 # as we don't actually return any commits. The creation of commits is
32 # handled in the tests themselves.
32 # handled in the tests themselves.
33 cls.nodes = [
33 cls.nodes = [
34 FileNode('foobar', content='Foo & bar'),
34 FileNode('foobar', content='Foo & bar'),
35 FileNode('foobar2', content='Foo & bar, doubled!'),
35 FileNode('foobar2', content='Foo & bar, doubled!'),
36 FileNode('foo bar with spaces', content=''),
36 FileNode('foo bar with spaces', content=''),
37 FileNode('foo/bar/baz', content='Inside'),
37 FileNode('foo/bar/baz', content='Inside'),
38 FileNode('foo/bar/file.bin', content='\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\x00\x03\x00\xfe\xff\t\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x18\x00\x00\x00\x01\x00\x00\x00\xfe\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'),
38 FileNode('foo/bar/file.bin', content='\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\x00\x03\x00\xfe\xff\t\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x18\x00\x00\x00\x01\x00\x00\x00\xfe\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'),
39 ]
39 ]
40 commits = []
40 commits = []
41 return commits
41 return commits
42
42
43 def test_add(self):
43 def test_add(self):
44 rev_count = len(self.repo.revisions)
44 rev_count = len(self.repo.revisions)
45 to_add = [FileNode(node.path, content=node.content)
45 to_add = [FileNode(node.path, content=node.content)
46 for node in self.nodes]
46 for node in self.nodes]
47 for node in to_add:
47 for node in to_add:
48 self.imc.add(node)
48 self.imc.add(node)
49 message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
49 message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
50 author = unicode(self.__class__)
50 author = unicode(self.__class__)
51 changeset = self.imc.commit(message=message, author=author)
51 changeset = self.imc.commit(message=message, author=author)
52
52
53 newtip = self.repo.get_changeset()
53 newtip = self.repo.get_changeset()
54 assert changeset == newtip
54 assert changeset == newtip
55 assert rev_count + 1 == len(self.repo.revisions)
55 assert rev_count + 1 == len(self.repo.revisions)
56 assert newtip.message == message
56 assert newtip.message == message
57 assert newtip.author == author
57 assert newtip.author == author
58 assert not any((
58 assert not any((
59 self.imc.added,
59 self.imc.added,
60 self.imc.changed,
60 self.imc.changed,
61 self.imc.removed
61 self.imc.removed
62 ))
62 ))
63 for node in to_add:
63 for node in to_add:
64 assert newtip.get_node(node.path).content == node.content
64 assert newtip.get_node(node.path).content == node.content
65
65
66 def test_add_in_bulk(self):
66 def test_add_in_bulk(self):
67 rev_count = len(self.repo.revisions)
67 rev_count = len(self.repo.revisions)
68 to_add = [FileNode(node.path, content=node.content)
68 to_add = [FileNode(node.path, content=node.content)
69 for node in self.nodes]
69 for node in self.nodes]
70 self.imc.add(*to_add)
70 self.imc.add(*to_add)
71 message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
71 message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
72 author = unicode(self.__class__)
72 author = unicode(self.__class__)
73 changeset = self.imc.commit(message=message, author=author)
73 changeset = self.imc.commit(message=message, author=author)
74
74
75 newtip = self.repo.get_changeset()
75 newtip = self.repo.get_changeset()
76 assert changeset == newtip
76 assert changeset == newtip
77 assert rev_count + 1 == len(self.repo.revisions)
77 assert rev_count + 1 == len(self.repo.revisions)
78 assert newtip.message == message
78 assert newtip.message == message
79 assert newtip.author == author
79 assert newtip.author == author
80 assert not any((
80 assert not any((
81 self.imc.added,
81 self.imc.added,
82 self.imc.changed,
82 self.imc.changed,
83 self.imc.removed
83 self.imc.removed
84 ))
84 ))
85 for node in to_add:
85 for node in to_add:
86 assert newtip.get_node(node.path).content == node.content
86 assert newtip.get_node(node.path).content == node.content
87
87
88 def test_add_actually_adds_all_nodes_at_second_commit_too(self):
88 def test_add_actually_adds_all_nodes_at_second_commit_too(self):
89 self.imc.add(FileNode('foo/bar/image.png', content='\0'))
89 self.imc.add(FileNode('foo/bar/image.png', content='\0'))
90 self.imc.add(FileNode('foo/README.txt', content='readme!'))
90 self.imc.add(FileNode('foo/README.txt', content='readme!'))
91 changeset = self.imc.commit(u'Initial', u'joe.doe@example.com')
91 changeset = self.imc.commit(u'Initial', u'joe.doe@example.com')
92 assert isinstance(changeset.get_node('foo'), DirNode)
92 assert isinstance(changeset.get_node('foo'), DirNode)
93 assert isinstance(changeset.get_node('foo/bar'), DirNode)
93 assert isinstance(changeset.get_node('foo/bar'), DirNode)
94 assert changeset.get_node('foo/bar/image.png').content == '\0'
94 assert changeset.get_node('foo/bar/image.png').content == '\0'
95 assert changeset.get_node('foo/README.txt').content == 'readme!'
95 assert changeset.get_node('foo/README.txt').content == 'readme!'
96
96
97 # commit some more files again
97 # commit some more files again
98 to_add = [
98 to_add = [
99 FileNode('foo/bar/foobaz/bar', content='foo'),
99 FileNode('foo/bar/foobaz/bar', content='foo'),
100 FileNode('foo/bar/another/bar', content='foo'),
100 FileNode('foo/bar/another/bar', content='foo'),
101 FileNode('foo/baz.txt', content='foo'),
101 FileNode('foo/baz.txt', content='foo'),
102 FileNode('foobar/foobaz/file', content='foo'),
102 FileNode('foobar/foobaz/file', content='foo'),
103 FileNode('foobar/barbaz', content='foo'),
103 FileNode('foobar/barbaz', content='foo'),
104 ]
104 ]
105 self.imc.add(*to_add)
105 self.imc.add(*to_add)
106 changeset = self.imc.commit(u'Another', u'joe.doe@example.com')
106 changeset = self.imc.commit(u'Another', u'joe.doe@example.com')
107 changeset.get_node('foo/bar/foobaz/bar').content == 'foo'
107 changeset.get_node('foo/bar/foobaz/bar').content == 'foo'
108 changeset.get_node('foo/bar/another/bar').content == 'foo'
108 changeset.get_node('foo/bar/another/bar').content == 'foo'
109 changeset.get_node('foo/baz.txt').content == 'foo'
109 changeset.get_node('foo/baz.txt').content == 'foo'
110 changeset.get_node('foobar/foobaz/file').content == 'foo'
110 changeset.get_node('foobar/foobaz/file').content == 'foo'
111 changeset.get_node('foobar/barbaz').content == 'foo'
111 changeset.get_node('foobar/barbaz').content == 'foo'
112
112
113 def test_add_non_ascii_files(self):
113 def test_add_non_ascii_files(self):
114 rev_count = len(self.repo.revisions)
114 rev_count = len(self.repo.revisions)
115 to_add = [
115 to_add = [
116 FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='Δ‡Δ‡Δ‡Δ‡'),
116 FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='Δ‡Δ‡Δ‡Δ‡'),
117 FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'Δ‡Δ‡Δ‡Δ‡'),
117 FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'Δ‡Δ‡Δ‡Δ‡'),
118 ]
118 ]
119 for node in to_add:
119 for node in to_add:
120 self.imc.add(node)
120 self.imc.add(node)
121 message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
121 message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
122 author = unicode(self.__class__)
122 author = unicode(self.__class__)
123 changeset = self.imc.commit(message=message, author=author)
123 changeset = self.imc.commit(message=message, author=author)
124
124
125 newtip = self.repo.get_changeset()
125 newtip = self.repo.get_changeset()
126 assert changeset == newtip
126 assert changeset == newtip
127 assert rev_count + 1 == len(self.repo.revisions)
127 assert rev_count + 1 == len(self.repo.revisions)
128 assert newtip.message == message
128 assert newtip.message == message
129 assert newtip.author == author
129 assert newtip.author == author
130 assert not any((
130 assert not any((
131 self.imc.added,
131 self.imc.added,
132 self.imc.changed,
132 self.imc.changed,
133 self.imc.removed
133 self.imc.removed
134 ))
134 ))
135 for node in to_add:
135 for node in to_add:
136 assert newtip.get_node(node.path).content == node.content
136 assert newtip.get_node(node.path).content == node.content
137
137
138 def test_add_raise_already_added(self):
138 def test_add_raise_already_added(self):
139 node = FileNode('foobar', content='baz')
139 node = FileNode('foobar', content='baz')
140 self.imc.add(node)
140 self.imc.add(node)
141 with pytest.raises(NodeAlreadyAddedError):
141 with pytest.raises(NodeAlreadyAddedError):
142 self.imc.add(node)
142 self.imc.add(node)
143
143
144 def test_check_integrity_raise_already_exist(self):
144 def test_check_integrity_raise_already_exist(self):
145 node = FileNode('foobar', content='baz')
145 node = FileNode('foobar', content='baz')
146 self.imc.add(node)
146 self.imc.add(node)
147 self.imc.commit(message=u'Added foobar', author=unicode(self))
147 self.imc.commit(message=u'Added foobar', author=unicode(self))
148 self.imc.add(node)
148 self.imc.add(node)
149 with pytest.raises(NodeAlreadyExistsError):
149 with pytest.raises(NodeAlreadyExistsError):
150 self.imc.commit(message='new message',
150 self.imc.commit(message='new message',
151 author=str(self))
151 author=str(self))
152
152
153 def test_change(self):
153 def test_change(self):
154 self.imc.add(FileNode('foo/bar/baz', content='foo'))
154 self.imc.add(FileNode('foo/bar/baz', content='foo'))
155 self.imc.add(FileNode('foo/fbar', content='foobar'))
155 self.imc.add(FileNode('foo/fbar', content='foobar'))
156 tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
156 tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
157
157
158 # Change node's content
158 # Change node's content
159 node = FileNode('foo/bar/baz', content='My **changed** content')
159 node = FileNode('foo/bar/baz', content='My **changed** content')
160 self.imc.change(node)
160 self.imc.change(node)
161 self.imc.commit(u'Changed %s' % node.path, u'joe.doe@example.com')
161 self.imc.commit(u'Changed %s' % node.path, u'joe.doe@example.com')
162
162
163 newtip = self.repo.get_changeset()
163 newtip = self.repo.get_changeset()
164 assert tip != newtip
164 assert tip != newtip
165 assert tip.id != newtip.id
165 assert tip.id != newtip.id
166 assert newtip.get_node('foo/bar/baz').content == 'My **changed** content'
166 assert newtip.get_node('foo/bar/baz').content == 'My **changed** content'
167
167
168 def test_change_non_ascii(self):
168 def test_change_non_ascii(self):
169 to_add = [
169 to_add = [
170 FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='Δ‡Δ‡Δ‡Δ‡'),
170 FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='Δ‡Δ‡Δ‡Δ‡'),
171 FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'Δ‡Δ‡Δ‡Δ‡'),
171 FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'Δ‡Δ‡Δ‡Δ‡'),
172 ]
172 ]
173 for node in to_add:
173 for node in to_add:
174 self.imc.add(node)
174 self.imc.add(node)
175
175
176 tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
176 tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
177
177
178 # Change node's content
178 # Change node's content
179 node = FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='My **changed** content')
179 node = FileNode('ΕΌΓ³Ε‚wik/zwierzΔ…tko', content='My **changed** content')
180 self.imc.change(node)
180 self.imc.change(node)
181 self.imc.commit(u'Changed %s' % safe_unicode(node.path),
181 self.imc.commit(u'Changed %s' % safe_unicode(node.path),
182 u'joe.doe@example.com')
182 u'joe.doe@example.com')
183
183
184 node = FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'My **changed** content')
184 node = FileNode(u'ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni', content=u'My **changed** content')
185 self.imc.change(node)
185 self.imc.change(node)
186 self.imc.commit(u'Changed %s' % safe_unicode(node.path),
186 self.imc.commit(u'Changed %s' % safe_unicode(node.path),
187 u'joe.doe@example.com')
187 u'joe.doe@example.com')
188
188
189 newtip = self.repo.get_changeset()
189 newtip = self.repo.get_changeset()
190 assert tip != newtip
190 assert tip != newtip
191 assert tip.id != newtip.id
191 assert tip.id != newtip.id
192
192
193 assert newtip.get_node('ΕΌΓ³Ε‚wik/zwierzΔ…tko').content == 'My **changed** content'
193 assert newtip.get_node('ΕΌΓ³Ε‚wik/zwierzΔ…tko').content == 'My **changed** content'
194 assert newtip.get_node('ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni').content == 'My **changed** content'
194 assert newtip.get_node('ΕΌΓ³Ε‚wik/zwierzΔ…tko_uni').content == 'My **changed** content'
195
195
196 def test_change_raise_empty_repository(self):
196 def test_change_raise_empty_repository(self):
197 node = FileNode('foobar')
197 node = FileNode('foobar')
198 with pytest.raises(EmptyRepositoryError):
198 with pytest.raises(EmptyRepositoryError):
199 self.imc.change(node)
199 self.imc.change(node)
200
200
201 def test_check_integrity_change_raise_node_does_not_exist(self):
201 def test_check_integrity_change_raise_node_does_not_exist(self):
202 node = FileNode('foobar', content='baz')
202 node = FileNode('foobar', content='baz')
203 self.imc.add(node)
203 self.imc.add(node)
204 self.imc.commit(message=u'Added foobar', author=unicode(self))
204 self.imc.commit(message=u'Added foobar', author=unicode(self))
205 node = FileNode('not-foobar', content='')
205 node = FileNode('not-foobar', content='')
206 self.imc.change(node)
206 self.imc.change(node)
207 with pytest.raises(NodeDoesNotExistError):
207 with pytest.raises(NodeDoesNotExistError):
208 self.imc.commit(message='Changed not existing node', author=str(self))
208 self.imc.commit(message='Changed not existing node', author=str(self))
209
209
210 def test_change_raise_node_already_changed(self):
210 def test_change_raise_node_already_changed(self):
211 node = FileNode('foobar', content='baz')
211 node = FileNode('foobar', content='baz')
212 self.imc.add(node)
212 self.imc.add(node)
213 self.imc.commit(message=u'Added foobar', author=unicode(self))
213 self.imc.commit(message=u'Added foobar', author=unicode(self))
214 node = FileNode('foobar', content='more baz')
214 node = FileNode('foobar', content='more baz')
215 self.imc.change(node)
215 self.imc.change(node)
216 with pytest.raises(NodeAlreadyChangedError):
216 with pytest.raises(NodeAlreadyChangedError):
217 self.imc.change(node)
217 self.imc.change(node)
218
218
219 def test_check_integrity_change_raise_node_not_changed(self):
219 def test_check_integrity_change_raise_node_not_changed(self):
220 self.test_add() # Performs first commit
220 self.test_add() # Performs first commit
221
221
222 node = FileNode(self.nodes[0].path, content=self.nodes[0].content)
222 node = FileNode(self.nodes[0].path, content=self.nodes[0].content)
223 self.imc.change(node)
223 self.imc.change(node)
224 with pytest.raises(NodeNotChangedError):
224 with pytest.raises(NodeNotChangedError):
225 self.imc.commit(
225 self.imc.commit(
226 message=u'Trying to mark node as changed without touching it',
226 message=u'Trying to mark node as changed without touching it',
227 author=unicode(self)
227 author=unicode(self)
228 )
228 )
229
229
230 def test_change_raise_node_already_removed(self):
230 def test_change_raise_node_already_removed(self):
231 node = FileNode('foobar', content='baz')
231 node = FileNode('foobar', content='baz')
232 self.imc.add(node)
232 self.imc.add(node)
233 self.imc.commit(message=u'Added foobar', author=unicode(self))
233 self.imc.commit(message=u'Added foobar', author=unicode(self))
234 self.imc.remove(FileNode('foobar'))
234 self.imc.remove(FileNode('foobar'))
235 with pytest.raises(NodeAlreadyRemovedError):
235 with pytest.raises(NodeAlreadyRemovedError):
236 self.imc.change(node)
236 self.imc.change(node)
237
237
238 def test_remove(self):
238 def test_remove(self):
239 self.test_add() # Performs first commit
239 self.test_add() # Performs first commit
240
240
241 tip = self.repo.get_changeset()
241 tip = self.repo.get_changeset()
242 node = self.nodes[0]
242 node = self.nodes[0]
243 assert node.content == tip.get_node(node.path).content
243 assert node.content == tip.get_node(node.path).content
244 self.imc.remove(node)
244 self.imc.remove(node)
245 self.imc.commit(message=u'Removed %s' % node.path, author=unicode(self))
245 self.imc.commit(message=u'Removed %s' % node.path, author=unicode(self))
246
246
247 newtip = self.repo.get_changeset()
247 newtip = self.repo.get_changeset()
248 assert tip != newtip
248 assert tip != newtip
249 assert tip.id != newtip.id
249 assert tip.id != newtip.id
250 with pytest.raises(NodeDoesNotExistError):
250 with pytest.raises(NodeDoesNotExistError):
251 newtip.get_node(node.path)
251 newtip.get_node(node.path)
252
252
253 def test_remove_last_file_from_directory(self):
253 def test_remove_last_file_from_directory(self):
254 node = FileNode('omg/qwe/foo/bar', content='foobar')
254 node = FileNode('omg/qwe/foo/bar', content='foobar')
255 self.imc.add(node)
255 self.imc.add(node)
256 self.imc.commit(u'added', u'joe doe')
256 self.imc.commit(u'added', u'joe doe')
257
257
258 self.imc.remove(node)
258 self.imc.remove(node)
259 tip = self.imc.commit(u'removed', u'joe doe')
259 tip = self.imc.commit(u'removed', u'joe doe')
260 with pytest.raises(NodeDoesNotExistError):
260 with pytest.raises(NodeDoesNotExistError):
261 tip.get_node('omg/qwe/foo/bar')
261 tip.get_node('omg/qwe/foo/bar')
262
262
263 def test_remove_raise_node_does_not_exist(self):
263 def test_remove_raise_node_does_not_exist(self):
264 self.imc.remove(self.nodes[0])
264 self.imc.remove(self.nodes[0])
265 with pytest.raises(NodeDoesNotExistError):
265 with pytest.raises(NodeDoesNotExistError):
266 self.imc.commit(
266 self.imc.commit(
267 message='Trying to remove node at empty repository',
267 message='Trying to remove node at empty repository',
268 author=str(self)
268 author=str(self)
269 )
269 )
270
270
271 def test_check_integrity_remove_raise_node_does_not_exist(self):
271 def test_check_integrity_remove_raise_node_does_not_exist(self):
272 self.test_add() # Performs first commit
272 self.test_add() # Performs first commit
273
273
274 node = FileNode('no-such-file')
274 node = FileNode('no-such-file')
275 self.imc.remove(node)
275 self.imc.remove(node)
276 with pytest.raises(NodeDoesNotExistError):
276 with pytest.raises(NodeDoesNotExistError):
277 self.imc.commit(
277 self.imc.commit(
278 message=u'Trying to remove not existing node',
278 message=u'Trying to remove not existing node',
279 author=unicode(self)
279 author=unicode(self)
280 )
280 )
281
281
282 def test_remove_raise_node_already_removed(self):
282 def test_remove_raise_node_already_removed(self):
283 self.test_add() # Performs first commit
283 self.test_add() # Performs first commit
284
284
285 node = FileNode(self.nodes[0].path)
285 node = FileNode(self.nodes[0].path)
286 self.imc.remove(node)
286 self.imc.remove(node)
287 with pytest.raises(NodeAlreadyRemovedError):
287 with pytest.raises(NodeAlreadyRemovedError):
288 self.imc.remove(node)
288 self.imc.remove(node)
289
289
290 def test_remove_raise_node_already_changed(self):
290 def test_remove_raise_node_already_changed(self):
291 self.test_add() # Performs first commit
291 self.test_add() # Performs first commit
292
292
293 node = FileNode(self.nodes[0].path, content='Bending time')
293 node = FileNode(self.nodes[0].path, content='Bending time')
294 self.imc.change(node)
294 self.imc.change(node)
295 with pytest.raises(NodeAlreadyChangedError):
295 with pytest.raises(NodeAlreadyChangedError):
296 self.imc.remove(node)
296 self.imc.remove(node)
297
297
298 def test_reset(self):
298 def test_reset(self):
299 self.imc.add(FileNode('foo', content='bar'))
299 self.imc.add(FileNode('foo', content='bar'))
300 #self.imc.change(FileNode('baz', content='new'))
300 #self.imc.change(FileNode('baz', content='new'))
301 #self.imc.remove(FileNode('qwe'))
301 #self.imc.remove(FileNode('qwe'))
302 self.imc.reset()
302 self.imc.reset()
303 assert not any((
303 assert not any((
304 self.imc.added,
304 self.imc.added,
305 self.imc.changed,
305 self.imc.changed,
306 self.imc.removed
306 self.imc.removed
307 ))
307 ))
308
308
309 def test_multiple_commits(self):
309 def test_multiple_commits(self):
310 N = 3 # number of commits to perform
310 N = 3 # number of commits to perform
311 last = None
311 last = None
312 for x in xrange(N):
312 for x in xrange(N):
313 fname = 'file%s' % str(x).rjust(5, '0')
313 fname = 'file%s' % str(x).rjust(5, '0')
314 content = 'foobar\n' * x
314 content = 'foobar\n' * x
315 node = FileNode(fname, content=content)
315 node = FileNode(fname, content=content)
316 self.imc.add(node)
316 self.imc.add(node)
317 commit = self.imc.commit(u"Commit no. %s" % (x + 1), author=u'vcs')
317 commit = self.imc.commit(u"Commit no. %s" % (x + 1), author=u'vcs')
318 assert last != commit
318 assert last != commit
319 last = commit
319 last = commit
320
320
321 # Check commit number for same repo
321 # Check commit number for same repo
322 assert len(self.repo.revisions) == N
322 assert len(self.repo.revisions) == N
323
323
324 # Check commit number for recreated repo
324 # Check commit number for recreated repo
325 assert len(self.repo.revisions) == N
325 assert len(self.repo.revisions) == N
326
326
327 def test_date_attr(self):
327 def test_date_attr(self):
328 node = FileNode('foobar.txt', content='Foobared!')
328 node = FileNode('foobar.txt', content='Foobared!')
329 self.imc.add(node)
329 self.imc.add(node)
330 date = datetime.datetime(1985, 1, 30, 1, 45)
330 date = datetime.datetime(1985, 1, 30, 1, 45)
331 commit = self.imc.commit(u"Committed at time when I was born ;-)",
331 commit = self.imc.commit(u"Committed at time when I was born ;-)",
332 author=u'lb <lb@example.com>', date=date)
332 author=u'lb <lb@example.com>', date=date)
333
333
334 assert commit.date == date
334 assert commit.date == date
335
335
336
336
337 class TestGitInMemoryChangeset(InMemoryChangesetTestMixin):
337 class TestGitInMemoryChangeset(InMemoryChangesetTestMixin):
338 backend_alias = 'git'
338 backend_alias = 'git'
339
339
340
340
341 class TestHgInMemoryChangeset(InMemoryChangesetTestMixin):
341 class TestHgInMemoryChangeset(InMemoryChangesetTestMixin):
342 backend_alias = 'hg'
342 backend_alias = 'hg'
@@ -1,253 +1,253 b''
1 #!/usr/bin/env python2
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3
3
4 """
4 """
5 Kallithea script for maintaining contributor lists from version control
5 Kallithea script for maintaining contributor lists from version control
6 history.
6 history.
7
7
8 This script and the data in it is a best effort attempt at reverse engineering
8 This script and the data in it is a best effort attempt at reverse engineering
9 previous attributions and correlate that with version control history while
9 previous attributions and correlate that with version control history while
10 preserving all existing copyright statements and attribution. This script is
10 preserving all existing copyright statements and attribution. This script is
11 processing and summarizing information found elsewhere - it is not by itself
11 processing and summarizing information found elsewhere - it is not by itself
12 making any claims. Comments in the script are an attempt at reverse engineering
12 making any claims. Comments in the script are an attempt at reverse engineering
13 possible explanations - they are not showing any intent or confirming it is
13 possible explanations - they are not showing any intent or confirming it is
14 correct.
14 correct.
15
15
16 Three files are generated / modified by this script:
16 Three files are generated / modified by this script:
17
17
18 kallithea/templates/about.html claims to show copyright holders, and the GPL
18 kallithea/templates/about.html claims to show copyright holders, and the GPL
19 license requires such existing "legal notices" to be preserved. We also try to
19 license requires such existing "legal notices" to be preserved. We also try to
20 keep it updated with copyright holders, but do not claim it is a correct list.
20 keep it updated with copyright holders, but do not claim it is a correct list.
21
21
22 CONTRIBUTORS has the purpose of giving credit where credit is due and list all
22 CONTRIBUTORS has the purpose of giving credit where credit is due and list all
23 the contributor names in the source.
23 the contributor names in the source.
24
24
25 kallithea/templates/base/base.html contains the copyright years in the page
25 kallithea/templates/base/base.html contains the copyright years in the page
26 footer.
26 footer.
27
27
28 Both make a best effort of listing all copyright holders, but revision control
28 Both make a best effort of listing all copyright holders, but revision control
29 history might be a better and more definitive source.
29 history might be a better and more definitive source.
30
30
31 Contributors are sorted "fairly" by copyright year and amount of
31 Contributors are sorted "fairly" by copyright year and amount of
32 contribution.
32 contribution.
33
33
34 New contributors are listed, without considering if the contribution contains
34 New contributors are listed, without considering if the contribution contains
35 copyrightable work.
35 copyrightable work.
36
36
37 When the copyright might belong to a different legal entity than the
37 When the copyright might belong to a different legal entity than the
38 contributor, the legal entity is given credit too.
38 contributor, the legal entity is given credit too.
39 """
39 """
40
40
41
41
42 # Some committers are so wrong that it doesn't point at any contributor:
42 # Some committers are so wrong that it doesn't point at any contributor:
43 total_ignore = set()
43 total_ignore = set()
44 total_ignore.add('*** failed to import extension hggit: No module named hggit')
44 total_ignore.add('*** failed to import extension hggit: No module named hggit')
45 total_ignore.add('<>')
45 total_ignore.add('<>')
46
46
47 # Normalize some committer names where people have contributed under different
47 # Normalize some committer names where people have contributed under different
48 # names or email addresses:
48 # names or email addresses:
49 name_fixes = {}
49 name_fixes = {}
50 name_fixes['Andrew Shadura'] = "Andrew Shadura <andrew@shadura.me>"
50 name_fixes['Andrew Shadura'] = "Andrew Shadura <andrew@shadura.me>"
51 name_fixes['aparkar'] = "Aparkar <aparkar@icloud.com>"
51 name_fixes['aparkar'] = "Aparkar <aparkar@icloud.com>"
52 name_fixes['Aras Pranckevicius'] = "Aras Pranckevičius <aras@unity3d.com>"
52 name_fixes['Aras Pranckevicius'] = "Aras Pranckevičius <aras@unity3d.com>"
53 name_fixes['Augosto Hermann'] = "Augusto Herrmann <augusto.herrmann@planejamento.gov.br>"
53 name_fixes['Augosto Hermann'] = "Augusto Herrmann <augusto.herrmann@planejamento.gov.br>"
54 name_fixes['"Bradley M. Kuhn" <bkuhn@ebb.org>'] = "Bradley M. Kuhn <bkuhn@sfconservancy.org>"
54 name_fixes['"Bradley M. Kuhn" <bkuhn@ebb.org>'] = "Bradley M. Kuhn <bkuhn@sfconservancy.org>"
55 name_fixes['dmitri.kuznetsov'] = "Dmitri Kuznetsov"
55 name_fixes['dmitri.kuznetsov'] = "Dmitri Kuznetsov"
56 name_fixes['Dmitri Kuznetsov'] = "Dmitri Kuznetsov"
56 name_fixes['Dmitri Kuznetsov'] = "Dmitri Kuznetsov"
57 name_fixes['domruf'] = "Dominik Ruf <dominikruf@gmail.com>"
57 name_fixes['domruf'] = "Dominik Ruf <dominikruf@gmail.com>"
58 name_fixes['Ingo von borstel'] = "Ingo von Borstel <kallithea@planetmaker.de>"
58 name_fixes['Ingo von borstel'] = "Ingo von Borstel <kallithea@planetmaker.de>"
59 name_fixes['Jan Heylen'] = "Jan Heylen <heyleke@gmail.com>"
59 name_fixes['Jan Heylen'] = "Jan Heylen <heyleke@gmail.com>"
60 name_fixes['Jason F. Harris'] = "Jason Harris <jason@jasonfharris.com>"
60 name_fixes['Jason F. Harris'] = "Jason Harris <jason@jasonfharris.com>"
61 name_fixes['Jelmer Vernooij'] = "Jelmer VernooΔ³ <jelmer@samba.org>"
61 name_fixes['Jelmer Vernooij'] = "Jelmer VernooΔ³ <jelmer@samba.org>"
62 name_fixes['jfh <jason@jasonfharris.com>'] = "Jason Harris <jason@jasonfharris.com>"
62 name_fixes['jfh <jason@jasonfharris.com>'] = "Jason Harris <jason@jasonfharris.com>"
63 name_fixes['Leonardo Carneiro<leonardo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>"
63 name_fixes['Leonardo Carneiro<leonardo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>"
64 name_fixes['leonardo'] = "Leonardo Carneiro <leonardo@unity3d.com>"
64 name_fixes['leonardo'] = "Leonardo Carneiro <leonardo@unity3d.com>"
65 name_fixes['Leonardo <leo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>"
65 name_fixes['Leonardo <leo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>"
66 name_fixes['Les Peabody'] = "Les Peabody <lpeabody@gmail.com>"
66 name_fixes['Les Peabody'] = "Les Peabody <lpeabody@gmail.com>"
67 name_fixes['"Lorenzo M. Catucci" <lorenzo@sancho.ccd.uniroma2.it>'] = "Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>"
67 name_fixes['"Lorenzo M. Catucci" <lorenzo@sancho.ccd.uniroma2.it>'] = "Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>"
68 name_fixes['Lukasz Balcerzak'] = "Łukasz Balcerzak <lukaszbalcerzak@gmail.com>"
68 name_fixes['Lukasz Balcerzak'] = "Łukasz Balcerzak <lukaszbalcerzak@gmail.com>"
69 name_fixes['mao <mao@lins.fju.edu.tw>'] = "Ching-Chen Mao <mao@lins.fju.edu.tw>"
69 name_fixes['mao <mao@lins.fju.edu.tw>'] = "Ching-Chen Mao <mao@lins.fju.edu.tw>"
70 name_fixes['marcink'] = "Marcin KuΕΊmiΕ„ski <marcin@python-works.com>"
70 name_fixes['marcink'] = "Marcin KuΕΊmiΕ„ski <marcin@python-works.com>"
71 name_fixes['Marcin Kuzminski'] = "Marcin KuΕΊmiΕ„ski <marcin@python-works.com>"
71 name_fixes['Marcin Kuzminski'] = "Marcin KuΕΊmiΕ„ski <marcin@python-works.com>"
72 name_fixes['nansenat16@null.tw'] = "nansenat16 <nansenat16@null.tw>"
72 name_fixes['nansenat16@null.tw'] = "nansenat16 <nansenat16@null.tw>"
73 name_fixes['Peter Vitt'] = "Peter Vitt <petervitt@web.de>"
73 name_fixes['Peter Vitt'] = "Peter Vitt <petervitt@web.de>"
74 name_fixes['philip.j@hostdime.com'] = "Philip Jameson <philip.j@hostdime.com>"
74 name_fixes['philip.j@hostdime.com'] = "Philip Jameson <philip.j@hostdime.com>"
75 name_fixes['SΓΈren LΓΈvborg'] = "SΓΈren LΓΈvborg <sorenl@unity3d.com>"
75 name_fixes['SΓΈren LΓΈvborg'] = "SΓΈren LΓΈvborg <sorenl@unity3d.com>"
76 name_fixes['Thomas De Schampheleire'] = "Thomas De Schampheleire <thomas.de_schampheleire@nokia.com>"
76 name_fixes['Thomas De Schampheleire'] = "Thomas De Schampheleire <thomas.de_schampheleire@nokia.com>"
77 name_fixes['Weblate'] = "<>"
77 name_fixes['Weblate'] = "<>"
78 name_fixes['xpol'] = "xpol <xpolife@gmail.com>"
78 name_fixes['xpol'] = "xpol <xpolife@gmail.com>"
79
79
80
80
81 # Some committer email address domains that indicate that another entity might
81 # Some committer email address domains that indicate that another entity might
82 # hold some copyright too:
82 # hold some copyright too:
83 domain_extra = {}
83 domain_extra = {}
84 domain_extra['unity3d.com'] = "Unity Technologies"
84 domain_extra['unity3d.com'] = "Unity Technologies"
85 domain_extra['rhodecode.com'] = "RhodeCode GmbH"
85 domain_extra['rhodecode.com'] = "RhodeCode GmbH"
86
86
87 # Repository history show some old contributions that traditionally hasn't been
87 # Repository history show some old contributions that traditionally hasn't been
88 # listed in about.html - preserve that:
88 # listed in about.html - preserve that:
89 no_about = set(total_ignore)
89 no_about = set(total_ignore)
90 # The following contributors were traditionally not listed in about.html and it
90 # The following contributors were traditionally not listed in about.html and it
91 # seems unclear if the copyright is personal or belongs to a company.
91 # seems unclear if the copyright is personal or belongs to a company.
92 no_about.add(('Thayne Harbaugh <thayne@fusionio.com>', '2011'))
92 no_about.add(('Thayne Harbaugh <thayne@fusionio.com>', '2011'))
93 no_about.add(('Dies Koper <diesk@fast.au.fujitsu.com>', '2012'))
93 no_about.add(('Dies Koper <diesk@fast.au.fujitsu.com>', '2012'))
94 no_about.add(('Erwin Kroon <e.kroon@smartmetersolutions.nl>', '2012'))
94 no_about.add(('Erwin Kroon <e.kroon@smartmetersolutions.nl>', '2012'))
95 no_about.add(('Vincent Caron <vcaron@bearstech.com>', '2012'))
95 no_about.add(('Vincent Caron <vcaron@bearstech.com>', '2012'))
96 # These contributors' contributions might be too small to be copyrightable:
96 # These contributors' contributions might be too small to be copyrightable:
97 no_about.add(('philip.j@hostdime.com', '2012'))
97 no_about.add(('philip.j@hostdime.com', '2012'))
98 no_about.add(('Stefan Engel <mail@engel-stefan.de>', '2012'))
98 no_about.add(('Stefan Engel <mail@engel-stefan.de>', '2012'))
99 no_about.add(('Ton Plomp <tcplomp@gmail.com>', '2013'))
99 no_about.add(('Ton Plomp <tcplomp@gmail.com>', '2013'))
100 # Was reworked and contributed later and shadowed by other contributions:
100 # Was reworked and contributed later and shadowed by other contributions:
101 no_about.add(('Sean Farley <sean.michael.farley@gmail.com>', '2013'))
101 no_about.add(('Sean Farley <sean.michael.farley@gmail.com>', '2013'))
102
102
103 # Preserve contributors listed in about.html but not appearing in repository
103 # Preserve contributors listed in about.html but not appearing in repository
104 # history:
104 # history:
105 other_about = [
105 other_about = [
106 ("2011", "Aparkar <aparkar@icloud.com>"),
106 ("2011", "Aparkar <aparkar@icloud.com>"),
107 ("2010", "RhodeCode GmbH"),
107 ("2010", "RhodeCode GmbH"),
108 ("2011", "RhodeCode GmbH"),
108 ("2011", "RhodeCode GmbH"),
109 ("2012", "RhodeCode GmbH"),
109 ("2012", "RhodeCode GmbH"),
110 ("2013", "RhodeCode GmbH"),
110 ("2013", "RhodeCode GmbH"),
111 ]
111 ]
112
112
113 # Preserve contributors listed in CONTRIBUTORS but not appearing in repository
113 # Preserve contributors listed in CONTRIBUTORS but not appearing in repository
114 # history:
114 # history:
115 other_contributors = [
115 other_contributors = [
116 ("", "Andrew Kesterson <andrew@aklabs.net>"),
116 ("", "Andrew Kesterson <andrew@aklabs.net>"),
117 ("", "cejones"),
117 ("", "cejones"),
118 ("", "David A. SjΓΈen <david.sjoen@westcon.no>"),
118 ("", "David A. SjΓΈen <david.sjoen@westcon.no>"),
119 ("", "James Rhodes <jrhodes@redpointsoftware.com.au>"),
119 ("", "James Rhodes <jrhodes@redpointsoftware.com.au>"),
120 ("", "Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>"),
120 ("", "Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>"),
121 ("", "larikale"),
121 ("", "larikale"),
122 ("", "RhodeCode GmbH"),
122 ("", "RhodeCode GmbH"),
123 ("", "Sebastian Kreutzberger <sebastian@rhodecode.com>"),
123 ("", "Sebastian Kreutzberger <sebastian@rhodecode.com>"),
124 ("", "Steve Romanow <slestak989@gmail.com>"),
124 ("", "Steve Romanow <slestak989@gmail.com>"),
125 ("", "SteveCohen"),
125 ("", "SteveCohen"),
126 ("", "Thomas <thomas@rhodecode.com>"),
126 ("", "Thomas <thomas@rhodecode.com>"),
127 ("", "Thomas Waldmann <tw-public@gmx.de>"),
127 ("", "Thomas Waldmann <tw-public@gmx.de>"),
128 ]
128 ]
129
129
130
130
131 import os
131 import os
132 import re
132 import re
133 from collections import defaultdict
133 from collections import defaultdict
134
134
135
135
136 def sortkey(x):
136 def sortkey(x):
137 """Return key for sorting contributors "fairly":
137 """Return key for sorting contributors "fairly":
138 * latest contribution
138 * latest contribution
139 * first contribution
139 * first contribution
140 * number of contribution years
140 * number of contribution years
141 * name (with some unicode normalization)
141 * name (with some unicode normalization)
142 The entries must be 2-tuples of a list of string years and the unicode name"""
142 The entries must be 2-tuples of a list of string years and the unicode name"""
143 return (x[0] and -int(x[0][-1]),
143 return (x[0] and -int(x[0][-1]),
144 x[0] and int(x[0][0]),
144 x[0] and int(x[0][0]),
145 -len(x[0]),
145 -len(x[0]),
146 x[1].decode('utf8').lower().replace(u'\xe9', u'e').replace(u'\u0142', u'l')
146 x[1].decode('utf-8').lower().replace(u'\xe9', u'e').replace(u'\u0142', u'l')
147 )
147 )
148
148
149
149
150 def nice_years(l, dash='-', join=' '):
150 def nice_years(l, dash='-', join=' '):
151 """Convert a list of years into brief range like '1900-1901, 1921'."""
151 """Convert a list of years into brief range like '1900-1901, 1921'."""
152 if not l:
152 if not l:
153 return ''
153 return ''
154 start = end = int(l[0])
154 start = end = int(l[0])
155 ranges = []
155 ranges = []
156 for year in l[1:] + [0]:
156 for year in l[1:] + [0]:
157 year = int(year)
157 year = int(year)
158 if year == end + 1:
158 if year == end + 1:
159 end = year
159 end = year
160 continue
160 continue
161 if start == end:
161 if start == end:
162 ranges.append('%s' % start)
162 ranges.append('%s' % start)
163 else:
163 else:
164 ranges.append('%s%s%s' % (start, dash, end))
164 ranges.append('%s%s%s' % (start, dash, end))
165 start = end = year
165 start = end = year
166 assert start == 0 and end == 0, (start, end)
166 assert start == 0 and end == 0, (start, end)
167 return join.join(ranges)
167 return join.join(ranges)
168
168
169
169
170 def insert_entries(
170 def insert_entries(
171 filename,
171 filename,
172 all_entries,
172 all_entries,
173 no_entries,
173 no_entries,
174 domain_extra,
174 domain_extra,
175 split_re,
175 split_re,
176 normalize_name,
176 normalize_name,
177 format_f):
177 format_f):
178 """Update file with contributor information.
178 """Update file with contributor information.
179 all_entries: list of tuples with year and name
179 all_entries: list of tuples with year and name
180 no_entries: set of names or name and year tuples to ignore
180 no_entries: set of names or name and year tuples to ignore
181 domain_extra: map domain name to extra credit name
181 domain_extra: map domain name to extra credit name
182 split_re: regexp matching the part of file to rewrite
182 split_re: regexp matching the part of file to rewrite
183 normalize_name: function to normalize names for grouping and display
183 normalize_name: function to normalize names for grouping and display
184 format_f: function formatting year list and name to a string
184 format_f: function formatting year list and name to a string
185 """
185 """
186 name_years = defaultdict(set)
186 name_years = defaultdict(set)
187
187
188 for year, name in all_entries:
188 for year, name in all_entries:
189 if name in no_entries or (name, year) in no_entries:
189 if name in no_entries or (name, year) in no_entries:
190 continue
190 continue
191 domain = name.split('@', 1)[-1].rstrip('>')
191 domain = name.split('@', 1)[-1].rstrip('>')
192 if domain in domain_extra:
192 if domain in domain_extra:
193 name_years[domain_extra[domain]].add(year)
193 name_years[domain_extra[domain]].add(year)
194 name_years[normalize_name(name)].add(year)
194 name_years[normalize_name(name)].add(year)
195
195
196 l = [(list(sorted(year for year in years if year)), name)
196 l = [(list(sorted(year for year in years if year)), name)
197 for name, years in name_years.items()]
197 for name, years in name_years.items()]
198 l.sort(key=sortkey)
198 l.sort(key=sortkey)
199
199
200 with open(filename) as f:
200 with open(filename) as f:
201 pre, post = re.split(split_re, f.read())
201 pre, post = re.split(split_re, f.read())
202
202
203 with open(filename, 'w') as f:
203 with open(filename, 'w') as f:
204 f.write(pre +
204 f.write(pre +
205 ''.join(format_f(years, name) for years, name in l) +
205 ''.join(format_f(years, name) for years, name in l) +
206 post)
206 post)
207
207
208
208
209 def main():
209 def main():
210 repo_entries = [
210 repo_entries = [
211 (year, name_fixes.get(name) or name_fixes.get(name.rsplit('<', 1)[0].strip()) or name)
211 (year, name_fixes.get(name) or name_fixes.get(name.rsplit('<', 1)[0].strip()) or name)
212 for year, name in
212 for year, name in
213 (line.strip().split(' ', 1)
213 (line.strip().split(' ', 1)
214 for line in os.popen("""hg log -r '::.' -T '{date(date,"%Y")} {author}\n'""").readlines())
214 for line in os.popen("""hg log -r '::.' -T '{date(date,"%Y")} {author}\n'""").readlines())
215 ]
215 ]
216
216
217 insert_entries(
217 insert_entries(
218 filename='kallithea/templates/about.html',
218 filename='kallithea/templates/about.html',
219 all_entries=repo_entries + other_about,
219 all_entries=repo_entries + other_about,
220 no_entries=no_about,
220 no_entries=no_about,
221 domain_extra=domain_extra,
221 domain_extra=domain_extra,
222 split_re=r'(?: <li>Copyright &copy; [^\n]*</li>\n)*',
222 split_re=r'(?: <li>Copyright &copy; [^\n]*</li>\n)*',
223 normalize_name=lambda name: name.split('<', 1)[0].strip(),
223 normalize_name=lambda name: name.split('<', 1)[0].strip(),
224 format_f=lambda years, name: ' <li>Copyright &copy; %s, %s</li>\n' % (nice_years(years, '&ndash;', ', '), name),
224 format_f=lambda years, name: ' <li>Copyright &copy; %s, %s</li>\n' % (nice_years(years, '&ndash;', ', '), name),
225 )
225 )
226
226
227 insert_entries(
227 insert_entries(
228 filename='CONTRIBUTORS',
228 filename='CONTRIBUTORS',
229 all_entries=repo_entries + other_contributors,
229 all_entries=repo_entries + other_contributors,
230 no_entries=total_ignore,
230 no_entries=total_ignore,
231 domain_extra=domain_extra,
231 domain_extra=domain_extra,
232 split_re=r'(?: [^\n]*\n)*',
232 split_re=r'(?: [^\n]*\n)*',
233 normalize_name=lambda name: name,
233 normalize_name=lambda name: name,
234 format_f=lambda years, name: (' %s%s%s\n' % (name, ' ' if years else '', nice_years(years))),
234 format_f=lambda years, name: (' %s%s%s\n' % (name, ' ' if years else '', nice_years(years))),
235 )
235 )
236
236
237 insert_entries(
237 insert_entries(
238 filename='kallithea/templates/base/base.html',
238 filename='kallithea/templates/base/base.html',
239 all_entries=repo_entries,
239 all_entries=repo_entries,
240 no_entries=total_ignore,
240 no_entries=total_ignore,
241 domain_extra={},
241 domain_extra={},
242 split_re=r'(?<=&copy;) .* (?=by various authors)',
242 split_re=r'(?<=&copy;) .* (?=by various authors)',
243 normalize_name=lambda name: '',
243 normalize_name=lambda name: '',
244 format_f=lambda years, name: ' ' + nice_years(years, '&ndash;', ', ') + ' ',
244 format_f=lambda years, name: ' ' + nice_years(years, '&ndash;', ', ') + ' ',
245 )
245 )
246
246
247
247
248 if __name__ == '__main__':
248 if __name__ == '__main__':
249 main()
249 main()
250
250
251
251
252 # To list new contributors since last tagging:
252 # To list new contributors since last tagging:
253 # { hg log -r '::tagged()' -T ' {author}\n {author}\n'; hg log -r '::.' -T ' {author}\n' | sort | uniq; } | sort | uniq -u
253 # { hg log -r '::tagged()' -T ' {author}\n {author}\n'; hg log -r '::.' -T ' {author}\n' | sort | uniq; } | sort | uniq -u
General Comments 0
You need to be logged in to leave comments. Login now