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