##// END OF EJS Templates
db: get rid of vcs_full_cache - it should always be used...
Mads Kiilerich -
r5739:6afa528e default
parent child Browse files
Show More
@@ -1,597 +1,594 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # Kallithea - Development config: #
4 4 # listening on *:5000 #
5 5 # sqlite and kallithea.db #
6 6 # initial_repo_scan = true #
7 7 # set debug = true #
8 8 # verbose and colorful logging #
9 9 # #
10 10 # The %(here)s variable will be replaced with the parent directory of this file#
11 11 ################################################################################
12 12 ################################################################################
13 13
14 14 [DEFAULT]
15 15 debug = true
16 16 pdebug = false
17 17
18 18 ################################################################################
19 19 ## Email settings ##
20 20 ## ##
21 21 ## Refer to the documentation ("Email settings") for more details. ##
22 22 ## ##
23 23 ## It is recommended to use a valid sender address that passes access ##
24 24 ## validation and spam filtering in mail servers. ##
25 25 ################################################################################
26 26
27 27 ## 'From' header for application emails. You can optionally add a name.
28 28 ## Default:
29 29 #app_email_from = Kallithea
30 30 ## Examples:
31 31 #app_email_from = Kallithea <kallithea-noreply@example.com>
32 32 #app_email_from = kallithea-noreply@example.com
33 33
34 34 ## Subject prefix for application emails.
35 35 ## A space between this prefix and the real subject is automatically added.
36 36 ## Default:
37 37 #email_prefix =
38 38 ## Example:
39 39 #email_prefix = [Kallithea]
40 40
41 41 ## Recipients for error emails and fallback recipients of application mails.
42 42 ## Multiple addresses can be specified, space-separated.
43 43 ## Only addresses are allowed, do not add any name part.
44 44 ## Default:
45 45 #email_to =
46 46 ## Examples:
47 47 #email_to = admin@example.com
48 48 #email_to = admin@example.com another_admin@example.com
49 49
50 50 ## 'From' header for error emails. You can optionally add a name.
51 51 ## Default:
52 52 #error_email_from = pylons@yourapp.com
53 53 ## Examples:
54 54 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
55 55 #error_email_from = paste_error@example.com
56 56
57 57 ## SMTP server settings
58 58 ## Only smtp_server is mandatory. All other settings take the specified default
59 59 ## values.
60 60 #smtp_server = smtp.example.com
61 61 #smtp_username =
62 62 #smtp_password =
63 63 #smtp_port = 25
64 64 #smtp_use_tls = false
65 65 #smtp_use_ssl = false
66 66 ## SMTP authentication parameters to use (e.g. LOGIN PLAIN CRAM-MD5, etc.).
67 67 ## If empty, use any of the authentication parameters supported by the server.
68 68 #smtp_auth =
69 69
70 70 [server:main]
71 71 ## PASTE ##
72 72 #use = egg:Paste#http
73 73 ## nr of worker threads to spawn
74 74 #threadpool_workers = 5
75 75 ## max request before thread respawn
76 76 #threadpool_max_requests = 10
77 77 ## option to use threads of process
78 78 #use_threadpool = true
79 79
80 80 ## WAITRESS ##
81 81 use = egg:waitress#main
82 82 ## number of worker threads
83 83 threads = 5
84 84 ## MAX BODY SIZE 100GB
85 85 max_request_body_size = 107374182400
86 86 ## use poll instead of select, fixes fd limits, may not work on old
87 87 ## windows systems.
88 88 #asyncore_use_poll = True
89 89
90 90 ## GUNICORN ##
91 91 #use = egg:gunicorn#main
92 92 ## number of process workers. You must set `instance_id = *` when this option
93 93 ## is set to more than one worker
94 94 #workers = 1
95 95 ## process name
96 96 #proc_name = kallithea
97 97 ## type of worker class, one of sync, eventlet, gevent, tornado
98 98 ## recommended for bigger setup is using of of other than sync one
99 99 #worker_class = sync
100 100 #max_requests = 1000
101 101 ## ammount of time a worker can handle request before it gets killed and
102 102 ## restarted
103 103 #timeout = 3600
104 104
105 105 ## UWSGI ##
106 106 ## run with uwsgi --ini-paste-logged <inifile.ini>
107 107 #[uwsgi]
108 108 #socket = /tmp/uwsgi.sock
109 109 #master = true
110 110 #http = 127.0.0.1:5000
111 111
112 112 ## set as deamon and redirect all output to file
113 113 #daemonize = ./uwsgi_kallithea.log
114 114
115 115 ## master process PID
116 116 #pidfile = ./uwsgi_kallithea.pid
117 117
118 118 ## stats server with workers statistics, use uwsgitop
119 119 ## for monitoring, `uwsgitop 127.0.0.1:1717`
120 120 #stats = 127.0.0.1:1717
121 121 #memory-report = true
122 122
123 123 ## log 5XX errors
124 124 #log-5xx = true
125 125
126 126 ## Set the socket listen queue size.
127 127 #listen = 256
128 128
129 129 ## Gracefully Reload workers after the specified amount of managed requests
130 130 ## (avoid memory leaks).
131 131 #max-requests = 1000
132 132
133 133 ## enable large buffers
134 134 #buffer-size = 65535
135 135
136 136 ## socket and http timeouts ##
137 137 #http-timeout = 3600
138 138 #socket-timeout = 3600
139 139
140 140 ## Log requests slower than the specified number of milliseconds.
141 141 #log-slow = 10
142 142
143 143 ## Exit if no app can be loaded.
144 144 #need-app = true
145 145
146 146 ## Set lazy mode (load apps in workers instead of master).
147 147 #lazy = true
148 148
149 149 ## scaling ##
150 150 ## set cheaper algorithm to use, if not set default will be used
151 151 #cheaper-algo = spare
152 152
153 153 ## minimum number of workers to keep at all times
154 154 #cheaper = 1
155 155
156 156 ## number of workers to spawn at startup
157 157 #cheaper-initial = 1
158 158
159 159 ## maximum number of workers that can be spawned
160 160 #workers = 4
161 161
162 162 ## how many workers should be spawned at a time
163 163 #cheaper-step = 1
164 164
165 165 ## COMMON ##
166 166 #host = 127.0.0.1
167 167 host = 0.0.0.0
168 168 port = 5000
169 169
170 170 ## middleware for hosting the WSGI application under a URL prefix
171 171 #[filter:proxy-prefix]
172 172 #use = egg:PasteDeploy#prefix
173 173 #prefix = /<your-prefix>
174 174
175 175 [app:main]
176 176 use = egg:kallithea
177 177 ## enable proxy prefix middleware
178 178 #filter-with = proxy-prefix
179 179
180 180 full_stack = true
181 181 static_files = true
182 182 ## Available Languages:
183 183 ## cs de fr hu ja nl_BE pl pt_BR ru sk zh_CN zh_TW
184 184 lang =
185 185 cache_dir = %(here)s/data
186 186 index_dir = %(here)s/data/index
187 187
188 188 ## perform a full repository scan on each server start, this should be
189 189 ## set to false after first startup, to allow faster server restarts.
190 190 #initial_repo_scan = false
191 191 initial_repo_scan = true
192 192
193 193 ## uncomment and set this path to use archive download cache
194 194 archive_cache_dir = %(here)s/tarballcache
195 195
196 196 ## change this to unique ID for security
197 197 app_instance_uuid = development-not-secret
198 198
199 199 ## cut off limit for large diffs (size in bytes)
200 200 cut_off_limit = 256000
201 201
202 ## use cache version of scm repo everywhere
203 vcs_full_cache = true
204
205 202 ## force https in Kallithea, fixes https redirects, assumes it's always https
206 203 force_https = false
207 204
208 205 ## use Strict-Transport-Security headers
209 206 use_htsts = false
210 207
211 208 ## number of commits stats will parse on each iteration
212 209 commit_parse_limit = 25
213 210
214 211 ## path to git executable
215 212 git_path = git
216 213
217 214 ## git rev filter option, --all is the default filter, if you need to
218 215 ## hide all refs in changelog switch this to --branches --tags
219 216 #git_rev_filter = --branches --tags
220 217
221 218 ## RSS feed options
222 219 rss_cut_off_limit = 256000
223 220 rss_items_per_page = 10
224 221 rss_include_diff = false
225 222
226 223 ## options for showing and identifying changesets
227 224 show_sha_length = 12
228 225 show_revision_number = false
229 226
230 227 ## gist URL alias, used to create nicer urls for gist. This should be an
231 228 ## url that does rewrites to _admin/gists/<gistid>.
232 229 ## example: http://gist.example.com/{gistid}. Empty means use the internal
233 230 ## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid>
234 231 gist_alias_url =
235 232
236 233 ## white list of API enabled controllers. This allows to add list of
237 234 ## controllers to which access will be enabled by api_key. eg: to enable
238 235 ## api access to raw_files put `FilesController:raw`, to enable access to patches
239 236 ## add `ChangesetController:changeset_patch`. This list should be "," separated
240 237 ## Syntax is <ControllerClass>:<function>. Check debug logs for generated names
241 238 ## Recommended settings below are commented out:
242 239 api_access_controllers_whitelist =
243 240 # ChangesetController:changeset_patch,
244 241 # ChangesetController:changeset_raw,
245 242 # FilesController:raw,
246 243 # FilesController:archivefile
247 244
248 245 ## default encoding used to convert from and to unicode
249 246 ## can be also a comma seperated list of encoding in case of mixed encodings
250 247 default_encoding = utf8
251 248
252 249 ## issue tracker for Kallithea (leave blank to disable, absent for default)
253 250 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
254 251
255 252 ## issue tracking mapping for commits messages
256 253 ## comment out issue_pat, issue_server, issue_prefix to enable
257 254
258 255 ## pattern to get the issues from commit messages
259 256 ## default one used here is #<numbers> with a regex passive group for `#`
260 257 ## {id} will be all groups matched from this pattern
261 258
262 259 issue_pat = (?:\s*#)(\d+)
263 260
264 261 ## server url to the issue, each {id} will be replaced with match
265 262 ## fetched from the regex and {repo} is replaced with full repository name
266 263 ## including groups {repo_name} is replaced with just name of repo
267 264
268 265 issue_server_link = https://issues.example.com/{repo}/issue/{id}
269 266
270 267 ## prefix to add to link to indicate it's an url
271 268 ## #314 will be replaced by <issue_prefix><id>
272 269
273 270 issue_prefix = #
274 271
275 272 ## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
276 273 ## multiple patterns, to other issues server, wiki or others
277 274 ## below an example how to create a wiki pattern
278 275 # wiki-some-id -> https://wiki.example.com/some-id
279 276
280 277 #issue_pat_wiki = (?:wiki-)(.+)
281 278 #issue_server_link_wiki = https://wiki.example.com/{id}
282 279 #issue_prefix_wiki = WIKI-
283 280
284 281 ## alternative return HTTP header for failed authentication. Default HTTP
285 282 ## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
286 283 ## handling that. Set this variable to 403 to return HTTPForbidden
287 284 auth_ret_code =
288 285
289 286 ## locking return code. When repository is locked return this HTTP code. 2XX
290 287 ## codes don't break the transactions while 4XX codes do
291 288 lock_ret_code = 423
292 289
293 290 ## allows to change the repository location in settings page
294 291 allow_repo_location_change = True
295 292
296 293 ## allows to setup custom hooks in settings page
297 294 allow_custom_hooks_settings = True
298 295
299 296 ## extra extensions for indexing, space separated and without the leading '.'.
300 297 # index.extensions =
301 298 # gemfile
302 299 # lock
303 300
304 301 ## extra filenames for indexing, space separated
305 302 # index.filenames =
306 303 # .dockerignore
307 304 # .editorconfig
308 305 # INSTALL
309 306 # CHANGELOG
310 307
311 308 ####################################
312 309 ### CELERY CONFIG ####
313 310 ####################################
314 311
315 312 use_celery = false
316 313 broker.host = localhost
317 314 broker.vhost = rabbitmqhost
318 315 broker.port = 5672
319 316 broker.user = rabbitmq
320 317 broker.password = qweqwe
321 318
322 319 celery.imports = kallithea.lib.celerylib.tasks
323 320
324 321 celery.result.backend = amqp
325 322 celery.result.dburi = amqp://
326 323 celery.result.serialier = json
327 324
328 325 #celery.send.task.error.emails = true
329 326 #celery.amqp.task.result.expires = 18000
330 327
331 328 celeryd.concurrency = 2
332 329 #celeryd.log.file = celeryd.log
333 330 celeryd.log.level = DEBUG
334 331 celeryd.max.tasks.per.child = 1
335 332
336 333 ## tasks will never be sent to the queue, but executed locally instead.
337 334 celery.always.eager = false
338 335
339 336 ####################################
340 337 ### BEAKER CACHE ####
341 338 ####################################
342 339
343 340 beaker.cache.data_dir = %(here)s/data/cache/data
344 341 beaker.cache.lock_dir = %(here)s/data/cache/lock
345 342
346 343 beaker.cache.regions = short_term,long_term,sql_cache_short
347 344
348 345 beaker.cache.short_term.type = memory
349 346 beaker.cache.short_term.expire = 60
350 347 beaker.cache.short_term.key_length = 256
351 348
352 349 beaker.cache.long_term.type = memory
353 350 beaker.cache.long_term.expire = 36000
354 351 beaker.cache.long_term.key_length = 256
355 352
356 353 beaker.cache.sql_cache_short.type = memory
357 354 beaker.cache.sql_cache_short.expire = 10
358 355 beaker.cache.sql_cache_short.key_length = 256
359 356
360 357 ####################################
361 358 ### BEAKER SESSION ####
362 359 ####################################
363 360
364 361 ## Name of session cookie. Should be unique for a given host and path, even when running
365 362 ## on different ports. Otherwise, cookie sessions will be shared and messed up.
366 363 beaker.session.key = kallithea
367 364 ## Sessions should always only be accessible by the browser, not directly by JavaScript.
368 365 beaker.session.httponly = true
369 366 ## Session lifetime. 2592000 seconds is 30 days.
370 367 beaker.session.timeout = 2592000
371 368
372 369 ## Server secret used with HMAC to ensure integrity of cookies.
373 370 beaker.session.secret = development-not-secret
374 371 ## Further, encrypt the data with AES.
375 372 #beaker.session.encrypt_key = <key_for_encryption>
376 373 #beaker.session.validate_key = <validation_key>
377 374
378 375 ## Type of storage used for the session, current types are
379 376 ## dbm, file, memcached, database, and memory.
380 377
381 378 ## File system storage of session data. (default)
382 379 #beaker.session.type = file
383 380
384 381 ## Cookie only, store all session data inside the cookie. Requires secure secrets.
385 382 #beaker.session.type = cookie
386 383
387 384 ## Database storage of session data.
388 385 #beaker.session.type = ext:database
389 386 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
390 387 #beaker.session.table_name = db_session
391 388
392 389 ############################
393 390 ## ERROR HANDLING SYSTEMS ##
394 391 ############################
395 392
396 393 ####################
397 394 ### [errormator] ###
398 395 ####################
399 396
400 397 ## Errormator is tailored to work with Kallithea, see
401 398 ## http://errormator.com for details how to obtain an account
402 399 ## you must install python package `errormator_client` to make it work
403 400
404 401 ## errormator enabled
405 402 errormator = false
406 403
407 404 errormator.server_url = https://api.errormator.com
408 405 errormator.api_key = YOUR_API_KEY
409 406
410 407 ## TWEAK AMOUNT OF INFO SENT HERE
411 408
412 409 ## enables 404 error logging (default False)
413 410 errormator.report_404 = false
414 411
415 412 ## time in seconds after request is considered being slow (default 1)
416 413 errormator.slow_request_time = 1
417 414
418 415 ## record slow requests in application
419 416 ## (needs to be enabled for slow datastore recording and time tracking)
420 417 errormator.slow_requests = true
421 418
422 419 ## enable hooking to application loggers
423 420 #errormator.logging = true
424 421
425 422 ## minimum log level for log capture
426 423 #errormator.logging.level = WARNING
427 424
428 425 ## send logs only from erroneous/slow requests
429 426 ## (saves API quota for intensive logging)
430 427 errormator.logging_on_error = false
431 428
432 429 ## list of additonal keywords that should be grabbed from environ object
433 430 ## can be string with comma separated list of words in lowercase
434 431 ## (by default client will always send following info:
435 432 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
436 433 ## start with HTTP* this list be extended with additional keywords here
437 434 errormator.environ_keys_whitelist =
438 435
439 436 ## list of keywords that should be blanked from request object
440 437 ## can be string with comma separated list of words in lowercase
441 438 ## (by default client will always blank keys that contain following words
442 439 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
443 440 ## this list be extended with additional keywords set here
444 441 errormator.request_keys_blacklist =
445 442
446 443 ## list of namespaces that should be ignores when gathering log entries
447 444 ## can be string with comma separated list of namespaces
448 445 ## (by default the client ignores own entries: errormator_client.client)
449 446 errormator.log_namespace_blacklist =
450 447
451 448 ################
452 449 ### [sentry] ###
453 450 ################
454 451
455 452 ## sentry is a alternative open source error aggregator
456 453 ## you must install python packages `sentry` and `raven` to enable
457 454
458 455 sentry.dsn = YOUR_DNS
459 456 sentry.servers =
460 457 sentry.name =
461 458 sentry.key =
462 459 sentry.public_key =
463 460 sentry.secret_key =
464 461 sentry.project =
465 462 sentry.site =
466 463 sentry.include_paths =
467 464 sentry.exclude_paths =
468 465
469 466 ################################################################################
470 467 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
471 468 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
472 469 ## execute malicious code after an exception is raised. ##
473 470 ################################################################################
474 471 #set debug = false
475 472 set debug = true
476 473
477 474 ##################################
478 475 ### LOGVIEW CONFIG ###
479 476 ##################################
480 477
481 478 logview.sqlalchemy = #faa
482 479 logview.pylons.templating = #bfb
483 480 logview.pylons.util = #eee
484 481
485 482 #########################################################
486 483 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
487 484 #########################################################
488 485
489 486 # SQLITE [default]
490 487 sqlalchemy.db1.url = sqlite:///%(here)s/kallithea.db?timeout=60
491 488
492 489 # POSTGRESQL
493 490 #sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
494 491
495 492 # MySQL
496 493 #sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea?charset=utf8
497 494
498 495 # see sqlalchemy docs for others
499 496
500 497 sqlalchemy.db1.echo = false
501 498 sqlalchemy.db1.pool_recycle = 3600
502 499
503 500 ################################
504 501 ### LOGGING CONFIGURATION ####
505 502 ################################
506 503
507 504 [loggers]
508 505 keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
509 506
510 507 [handlers]
511 508 keys = console, console_sql
512 509
513 510 [formatters]
514 511 keys = generic, color_formatter, color_formatter_sql
515 512
516 513 #############
517 514 ## LOGGERS ##
518 515 #############
519 516
520 517 [logger_root]
521 518 level = NOTSET
522 519 handlers = console
523 520
524 521 [logger_routes]
525 522 level = DEBUG
526 523 handlers =
527 524 qualname = routes.middleware
528 525 ## "level = DEBUG" logs the route matched and routing variables.
529 526 propagate = 1
530 527
531 528 [logger_beaker]
532 529 level = DEBUG
533 530 handlers =
534 531 qualname = beaker.container
535 532 propagate = 1
536 533
537 534 [logger_templates]
538 535 level = INFO
539 536 handlers =
540 537 qualname = pylons.templating
541 538 propagate = 1
542 539
543 540 [logger_kallithea]
544 541 level = DEBUG
545 542 handlers =
546 543 qualname = kallithea
547 544 propagate = 1
548 545
549 546 [logger_sqlalchemy]
550 547 level = INFO
551 548 handlers = console_sql
552 549 qualname = sqlalchemy.engine
553 550 propagate = 0
554 551
555 552 [logger_whoosh_indexer]
556 553 level = DEBUG
557 554 handlers =
558 555 qualname = whoosh_indexer
559 556 propagate = 1
560 557
561 558 ##############
562 559 ## HANDLERS ##
563 560 ##############
564 561
565 562 [handler_console]
566 563 class = StreamHandler
567 564 args = (sys.stderr,)
568 565 #level = INFO
569 566 level = DEBUG
570 567 #formatter = generic
571 568 formatter = color_formatter
572 569
573 570 [handler_console_sql]
574 571 class = StreamHandler
575 572 args = (sys.stderr,)
576 573 #level = WARN
577 574 level = DEBUG
578 575 #formatter = generic
579 576 formatter = color_formatter_sql
580 577
581 578 ################
582 579 ## FORMATTERS ##
583 580 ################
584 581
585 582 [formatter_generic]
586 583 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
587 584 datefmt = %Y-%m-%d %H:%M:%S
588 585
589 586 [formatter_color_formatter]
590 587 class = kallithea.lib.colored_formatter.ColorFormatter
591 588 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
592 589 datefmt = %Y-%m-%d %H:%M:%S
593 590
594 591 [formatter_color_formatter_sql]
595 592 class = kallithea.lib.colored_formatter.ColorFormatterSql
596 593 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
597 594 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,55 +1,51 b''
1 1 .. _performance:
2 2
3 3 ================================
4 4 Optimizing Kallithea performance
5 5 ================================
6 6
7 7 When serving a large amount of big repositories, Kallithea can start
8 8 performing slower than expected. Because of the demanding nature of handling large
9 9 amounts of data from version control systems, here are some tips on how to get
10 10 the best performance.
11 11
12 * Kallithea is often I/O bound, and hence a fast disk (SSD/SAN) is
13 usually more important than a fast CPU.
14
15 * Sluggish loading of the front page can easily be fixed by grouping repositories or by
16 increasing cache size (see below). This includes using the lightweight dashboard
17 option and ``vcs_full_cache`` setting in .ini file.
18
19 12 Follow these few steps to improve performance of Kallithea system.
20 13
21 1. Increase cache
14 1. Kallithea is often I/O bound, and hence a fast disk (SSD/SAN) is
15 usually more important than a fast CPU.
16
17 2. Increase cache
22 18
23 19 Tweak beaker cache settings in the ini file. The actual effect of that
24 20 is questionable.
25 21
26 2. Switch from SQLite to PostgreSQL or MySQL
22 3. Switch from SQLite to PostgreSQL or MySQL
27 23
28 24 SQLite is a good option when having a small load on the system. But due to
29 25 locking issues with SQLite, it is not recommended to use it for larger
30 26 deployments. Switching to MySQL or PostgreSQL will result in an immediate
31 27 performance increase. A tool like SQLAlchemyGrate_ can be used for
32 28 migrating to another database platform.
33 29
34 3. Scale Kallithea horizontally
30 4. Scale Kallithea horizontally
35 31
36 32 Scaling horizontally can give huge performance benefits when dealing with
37 33 large amounts of traffic (many users, CI servers, etc.). Kallithea can be
38 34 scaled horizontally on one (recommended) or multiple machines. In order
39 35 to scale horizontally you need to do the following:
40 36
41 37 - Each instance's ``data`` storage needs to be configured to be stored on a
42 38 shared disk storage, preferably together with repositories. This ``data``
43 39 dir contains template caches, sessions, whoosh index and is used for
44 40 task locking (so it is safe across multiple instances). Set the
45 41 ``cache_dir``, ``index_dir``, ``beaker.cache.data_dir``, ``beaker.cache.lock_dir``
46 42 variables in each .ini file to a shared location across Kallithea instances
47 43 - If celery is used each instance should run a separate Celery instance, but
48 44 the message broker should be common to all of them (e.g., one
49 45 shared RabbitMQ server)
50 46 - Load balance using round robin or IP hash, recommended is writing LB rules
51 47 that will separate regular user traffic from automated processes like CI
52 48 servers or build bots.
53 49
54 50
55 51 .. _SQLAlchemyGrate: https://github.com/shazow/sqlalchemygrate
@@ -1,596 +1,593 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 <%text>################################################################################</%text>
6 6 <%text>################################################################################</%text>
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 pdebug = false
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, space-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
44 44 <%text>## 'From' header for error emails. You can optionally add a name.</%text>
45 45 <%text>## Default:</%text>
46 46 #error_email_from = pylons@yourapp.com
47 47 <%text>## Examples:</%text>
48 48 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
49 49 #error_email_from = paste_error@example.com
50 50
51 51 <%text>## SMTP server settings</%text>
52 52 <%text>## Only smtp_server is mandatory. All other settings take the specified default</%text>
53 53 <%text>## values.</%text>
54 54 #smtp_server = smtp.example.com
55 55 #smtp_username =
56 56 #smtp_password =
57 57 #smtp_port = 25
58 58 #smtp_use_tls = false
59 59 #smtp_use_ssl = false
60 60 <%text>## SMTP authentication parameters to use (e.g. LOGIN PLAIN CRAM-MD5, etc.).</%text>
61 61 <%text>## If empty, use any of the authentication parameters supported by the server.</%text>
62 62 #smtp_auth =
63 63
64 64 [server:main]
65 65 %if http_server == 'paste':
66 66 <%text>## PASTE ##</%text>
67 67 use = egg:Paste#http
68 68 <%text>## nr of worker threads to spawn</%text>
69 69 threadpool_workers = 5
70 70 <%text>## max request before thread respawn</%text>
71 71 threadpool_max_requests = 10
72 72 <%text>## option to use threads of process</%text>
73 73 use_threadpool = true
74 74
75 75 %elif http_server == 'waitress':
76 76 <%text>## WAITRESS ##</%text>
77 77 use = egg:waitress#main
78 78 <%text>## number of worker threads</%text>
79 79 threads = 5
80 80 <%text>## MAX BODY SIZE 100GB</%text>
81 81 max_request_body_size = 107374182400
82 82 <%text>## use poll instead of select, fixes fd limits, may not work on old</%text>
83 83 <%text>## windows systems.</%text>
84 84 #asyncore_use_poll = True
85 85
86 86 %elif http_server == 'gunicorn':
87 87 <%text>## GUNICORN ##</%text>
88 88 use = egg:gunicorn#main
89 89 <%text>## number of process workers. You must set `instance_id = *` when this option</%text>
90 90 <%text>## is set to more than one worker</%text>
91 91 workers = 1
92 92 <%text>## process name</%text>
93 93 proc_name = kallithea
94 94 <%text>## type of worker class, one of sync, eventlet, gevent, tornado</%text>
95 95 <%text>## recommended for bigger setup is using of of other than sync one</%text>
96 96 worker_class = sync
97 97 max_requests = 1000
98 98 <%text>## ammount of time a worker can handle request before it gets killed and</%text>
99 99 <%text>## restarted</%text>
100 100 timeout = 3600
101 101
102 102 %elif http_server == 'uwsgi':
103 103 <%text>## UWSGI ##</%text>
104 104 <%text>## run with uwsgi --ini-paste-logged <inifile.ini></%text>
105 105 [uwsgi]
106 106 socket = /tmp/uwsgi.sock
107 107 master = true
108 108 http = 127.0.0.1:5000
109 109
110 110 <%text>## set as deamon and redirect all output to file</%text>
111 111 #daemonize = ./uwsgi_kallithea.log
112 112
113 113 <%text>## master process PID</%text>
114 114 pidfile = ./uwsgi_kallithea.pid
115 115
116 116 <%text>## stats server with workers statistics, use uwsgitop</%text>
117 117 <%text>## for monitoring, `uwsgitop 127.0.0.1:1717`</%text>
118 118 stats = 127.0.0.1:1717
119 119 memory-report = true
120 120
121 121 <%text>## log 5XX errors</%text>
122 122 log-5xx = true
123 123
124 124 <%text>## Set the socket listen queue size.</%text>
125 125 listen = 256
126 126
127 127 <%text>## Gracefully Reload workers after the specified amount of managed requests</%text>
128 128 <%text>## (avoid memory leaks).</%text>
129 129 max-requests = 1000
130 130
131 131 <%text>## enable large buffers</%text>
132 132 buffer-size = 65535
133 133
134 134 <%text>## socket and http timeouts ##</%text>
135 135 http-timeout = 3600
136 136 socket-timeout = 3600
137 137
138 138 <%text>## Log requests slower than the specified number of milliseconds.</%text>
139 139 log-slow = 10
140 140
141 141 <%text>## Exit if no app can be loaded.</%text>
142 142 need-app = true
143 143
144 144 <%text>## Set lazy mode (load apps in workers instead of master).</%text>
145 145 lazy = true
146 146
147 147 <%text>## scaling ##</%text>
148 148 <%text>## set cheaper algorithm to use, if not set default will be used</%text>
149 149 cheaper-algo = spare
150 150
151 151 <%text>## minimum number of workers to keep at all times</%text>
152 152 cheaper = 1
153 153
154 154 <%text>## number of workers to spawn at startup</%text>
155 155 cheaper-initial = 1
156 156
157 157 <%text>## maximum number of workers that can be spawned</%text>
158 158 workers = 4
159 159
160 160 <%text>## how many workers should be spawned at a time</%text>
161 161 cheaper-step = 1
162 162
163 163 %endif
164 164 <%text>## COMMON ##</%text>
165 165 host = ${host}
166 166 port = ${port}
167 167
168 168 <%text>## middleware for hosting the WSGI application under a URL prefix</%text>
169 169 #[filter:proxy-prefix]
170 170 #use = egg:PasteDeploy#prefix
171 171 #prefix = /<your-prefix>
172 172
173 173 [app:main]
174 174 use = egg:kallithea
175 175 <%text>## enable proxy prefix middleware</%text>
176 176 #filter-with = proxy-prefix
177 177
178 178 full_stack = true
179 179 static_files = true
180 180 <%text>## Available Languages:</%text>
181 181 <%text>## cs de fr hu ja nl_BE pl pt_BR ru sk zh_CN zh_TW</%text>
182 182 lang =
183 183 cache_dir = ${here}/data
184 184 index_dir = ${here}/data/index
185 185
186 186 <%text>## perform a full repository scan on each server start, this should be</%text>
187 187 <%text>## set to false after first startup, to allow faster server restarts.</%text>
188 188 initial_repo_scan = false
189 189
190 190 <%text>## uncomment and set this path to use archive download cache</%text>
191 191 archive_cache_dir = ${here}/tarballcache
192 192
193 193 <%text>## change this to unique ID for security</%text>
194 194 app_instance_uuid = ${uuid()}
195 195
196 196 <%text>## cut off limit for large diffs (size in bytes)</%text>
197 197 cut_off_limit = 256000
198 198
199 <%text>## use cache version of scm repo everywhere</%text>
200 vcs_full_cache = true
201
202 199 <%text>## force https in Kallithea, fixes https redirects, assumes it's always https</%text>
203 200 force_https = false
204 201
205 202 <%text>## use Strict-Transport-Security headers</%text>
206 203 use_htsts = false
207 204
208 205 <%text>## number of commits stats will parse on each iteration</%text>
209 206 commit_parse_limit = 25
210 207
211 208 <%text>## path to git executable</%text>
212 209 git_path = git
213 210
214 211 <%text>## git rev filter option, --all is the default filter, if you need to</%text>
215 212 <%text>## hide all refs in changelog switch this to --branches --tags</%text>
216 213 #git_rev_filter = --branches --tags
217 214
218 215 <%text>## RSS feed options</%text>
219 216 rss_cut_off_limit = 256000
220 217 rss_items_per_page = 10
221 218 rss_include_diff = false
222 219
223 220 <%text>## options for showing and identifying changesets</%text>
224 221 show_sha_length = 12
225 222 show_revision_number = false
226 223
227 224 <%text>## gist URL alias, used to create nicer urls for gist. This should be an</%text>
228 225 <%text>## url that does rewrites to _admin/gists/<gistid>.</%text>
229 226 <%text>## example: http://gist.example.com/{gistid}. Empty means use the internal</%text>
230 227 <%text>## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid></%text>
231 228 gist_alias_url =
232 229
233 230 <%text>## white list of API enabled controllers. This allows to add list of</%text>
234 231 <%text>## controllers to which access will be enabled by api_key. eg: to enable</%text>
235 232 <%text>## api access to raw_files put `FilesController:raw`, to enable access to patches</%text>
236 233 <%text>## add `ChangesetController:changeset_patch`. This list should be "," separated</%text>
237 234 <%text>## Syntax is <ControllerClass>:<function>. Check debug logs for generated names</%text>
238 235 <%text>## Recommended settings below are commented out:</%text>
239 236 api_access_controllers_whitelist =
240 237 # ChangesetController:changeset_patch,
241 238 # ChangesetController:changeset_raw,
242 239 # FilesController:raw,
243 240 # FilesController:archivefile
244 241
245 242 <%text>## default encoding used to convert from and to unicode</%text>
246 243 <%text>## can be also a comma seperated list of encoding in case of mixed encodings</%text>
247 244 default_encoding = utf8
248 245
249 246 <%text>## issue tracker for Kallithea (leave blank to disable, absent for default)</%text>
250 247 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
251 248
252 249 <%text>## issue tracking mapping for commits messages</%text>
253 250 <%text>## comment out issue_pat, issue_server, issue_prefix to enable</%text>
254 251
255 252 <%text>## pattern to get the issues from commit messages</%text>
256 253 <%text>## default one used here is #<numbers> with a regex passive group for `#`</%text>
257 254 <%text>## {id} will be all groups matched from this pattern</%text>
258 255
259 256 issue_pat = (?:\s*#)(\d+)
260 257
261 258 <%text>## server url to the issue, each {id} will be replaced with match</%text>
262 259 <%text>## fetched from the regex and {repo} is replaced with full repository name</%text>
263 260 <%text>## including groups {repo_name} is replaced with just name of repo</%text>
264 261
265 262 issue_server_link = https://issues.example.com/{repo}/issue/{id}
266 263
267 264 <%text>## prefix to add to link to indicate it's an url</%text>
268 265 <%text>## #314 will be replaced by <issue_prefix><id></%text>
269 266
270 267 issue_prefix = #
271 268
272 269 <%text>## issue_pat, issue_server_link, issue_prefix can have suffixes to specify</%text>
273 270 <%text>## multiple patterns, to other issues server, wiki or others</%text>
274 271 <%text>## below an example how to create a wiki pattern</%text>
275 272 # wiki-some-id -> https://wiki.example.com/some-id
276 273
277 274 #issue_pat_wiki = (?:wiki-)(.+)
278 275 #issue_server_link_wiki = https://wiki.example.com/{id}
279 276 #issue_prefix_wiki = WIKI-
280 277
281 278 <%text>## alternative return HTTP header for failed authentication. Default HTTP</%text>
282 279 <%text>## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with</%text>
283 280 <%text>## handling that. Set this variable to 403 to return HTTPForbidden</%text>
284 281 auth_ret_code =
285 282
286 283 <%text>## locking return code. When repository is locked return this HTTP code. 2XX</%text>
287 284 <%text>## codes don't break the transactions while 4XX codes do</%text>
288 285 lock_ret_code = 423
289 286
290 287 <%text>## allows to change the repository location in settings page</%text>
291 288 allow_repo_location_change = True
292 289
293 290 <%text>## allows to setup custom hooks in settings page</%text>
294 291 allow_custom_hooks_settings = True
295 292
296 293 <%text>## extra extensions for indexing, space separated and without the leading '.'.</%text>
297 294 # index.extensions =
298 295 # gemfile
299 296 # lock
300 297
301 298 <%text>## extra filenames for indexing, space separated</%text>
302 299 # index.filenames =
303 300 # .dockerignore
304 301 # .editorconfig
305 302 # INSTALL
306 303 # CHANGELOG
307 304
308 305 <%text>####################################</%text>
309 306 <%text>### CELERY CONFIG ####</%text>
310 307 <%text>####################################</%text>
311 308
312 309 use_celery = false
313 310 broker.host = localhost
314 311 broker.vhost = rabbitmqhost
315 312 broker.port = 5672
316 313 broker.user = rabbitmq
317 314 broker.password = qweqwe
318 315
319 316 celery.imports = kallithea.lib.celerylib.tasks
320 317
321 318 celery.result.backend = amqp
322 319 celery.result.dburi = amqp://
323 320 celery.result.serialier = json
324 321
325 322 #celery.send.task.error.emails = true
326 323 #celery.amqp.task.result.expires = 18000
327 324
328 325 celeryd.concurrency = 2
329 326 #celeryd.log.file = celeryd.log
330 327 celeryd.log.level = DEBUG
331 328 celeryd.max.tasks.per.child = 1
332 329
333 330 <%text>## tasks will never be sent to the queue, but executed locally instead.</%text>
334 331 celery.always.eager = false
335 332
336 333 <%text>####################################</%text>
337 334 <%text>### BEAKER CACHE ####</%text>
338 335 <%text>####################################</%text>
339 336
340 337 beaker.cache.data_dir = ${here}/data/cache/data
341 338 beaker.cache.lock_dir = ${here}/data/cache/lock
342 339
343 340 beaker.cache.regions = short_term,long_term,sql_cache_short
344 341
345 342 beaker.cache.short_term.type = memory
346 343 beaker.cache.short_term.expire = 60
347 344 beaker.cache.short_term.key_length = 256
348 345
349 346 beaker.cache.long_term.type = memory
350 347 beaker.cache.long_term.expire = 36000
351 348 beaker.cache.long_term.key_length = 256
352 349
353 350 beaker.cache.sql_cache_short.type = memory
354 351 beaker.cache.sql_cache_short.expire = 10
355 352 beaker.cache.sql_cache_short.key_length = 256
356 353
357 354 <%text>####################################</%text>
358 355 <%text>### BEAKER SESSION ####</%text>
359 356 <%text>####################################</%text>
360 357
361 358 <%text>## Name of session cookie. Should be unique for a given host and path, even when running</%text>
362 359 <%text>## on different ports. Otherwise, cookie sessions will be shared and messed up.</%text>
363 360 beaker.session.key = kallithea
364 361 <%text>## Sessions should always only be accessible by the browser, not directly by JavaScript.</%text>
365 362 beaker.session.httponly = true
366 363 <%text>## Session lifetime. 2592000 seconds is 30 days.</%text>
367 364 beaker.session.timeout = 2592000
368 365
369 366 <%text>## Server secret used with HMAC to ensure integrity of cookies.</%text>
370 367 beaker.session.secret = ${uuid()}
371 368 <%text>## Further, encrypt the data with AES.</%text>
372 369 #beaker.session.encrypt_key = <key_for_encryption>
373 370 #beaker.session.validate_key = <validation_key>
374 371
375 372 <%text>## Type of storage used for the session, current types are</%text>
376 373 <%text>## dbm, file, memcached, database, and memory.</%text>
377 374
378 375 <%text>## File system storage of session data. (default)</%text>
379 376 #beaker.session.type = file
380 377
381 378 <%text>## Cookie only, store all session data inside the cookie. Requires secure secrets.</%text>
382 379 #beaker.session.type = cookie
383 380
384 381 <%text>## Database storage of session data.</%text>
385 382 #beaker.session.type = ext:database
386 383 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
387 384 #beaker.session.table_name = db_session
388 385
389 386 %if error_aggregation_service == 'errormator':
390 387 <%text>############################</%text>
391 388 <%text>## ERROR HANDLING SYSTEMS ##</%text>
392 389 <%text>############################</%text>
393 390
394 391 <%text>####################</%text>
395 392 <%text>### [errormator] ###</%text>
396 393 <%text>####################</%text>
397 394
398 395 <%text>## Errormator is tailored to work with Kallithea, see</%text>
399 396 <%text>## http://errormator.com for details how to obtain an account</%text>
400 397 <%text>## you must install python package `errormator_client` to make it work</%text>
401 398
402 399 <%text>## errormator enabled</%text>
403 400 errormator = false
404 401
405 402 errormator.server_url = https://api.errormator.com
406 403 errormator.api_key = YOUR_API_KEY
407 404
408 405 <%text>## TWEAK AMOUNT OF INFO SENT HERE</%text>
409 406
410 407 <%text>## enables 404 error logging (default False)</%text>
411 408 errormator.report_404 = false
412 409
413 410 <%text>## time in seconds after request is considered being slow (default 1)</%text>
414 411 errormator.slow_request_time = 1
415 412
416 413 <%text>## record slow requests in application</%text>
417 414 <%text>## (needs to be enabled for slow datastore recording and time tracking)</%text>
418 415 errormator.slow_requests = true
419 416
420 417 <%text>## enable hooking to application loggers</%text>
421 418 #errormator.logging = true
422 419
423 420 <%text>## minimum log level for log capture</%text>
424 421 #errormator.logging.level = WARNING
425 422
426 423 <%text>## send logs only from erroneous/slow requests</%text>
427 424 <%text>## (saves API quota for intensive logging)</%text>
428 425 errormator.logging_on_error = false
429 426
430 427 <%text>## list of additonal keywords that should be grabbed from environ object</%text>
431 428 <%text>## can be string with comma separated list of words in lowercase</%text>
432 429 <%text>## (by default client will always send following info:</%text>
433 430 <%text>## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that</%text>
434 431 <%text>## start with HTTP* this list be extended with additional keywords here</%text>
435 432 errormator.environ_keys_whitelist =
436 433
437 434 <%text>## list of keywords that should be blanked from request object</%text>
438 435 <%text>## can be string with comma separated list of words in lowercase</%text>
439 436 <%text>## (by default client will always blank keys that contain following words</%text>
440 437 <%text>## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'</%text>
441 438 <%text>## this list be extended with additional keywords set here</%text>
442 439 errormator.request_keys_blacklist =
443 440
444 441 <%text>## list of namespaces that should be ignores when gathering log entries</%text>
445 442 <%text>## can be string with comma separated list of namespaces</%text>
446 443 <%text>## (by default the client ignores own entries: errormator_client.client)</%text>
447 444 errormator.log_namespace_blacklist =
448 445
449 446 %elif error_aggregation_service == 'sentry':
450 447 <%text>################</%text>
451 448 <%text>### [sentry] ###</%text>
452 449 <%text>################</%text>
453 450
454 451 <%text>## sentry is a alternative open source error aggregator</%text>
455 452 <%text>## you must install python packages `sentry` and `raven` to enable</%text>
456 453
457 454 sentry.dsn = YOUR_DNS
458 455 sentry.servers =
459 456 sentry.name =
460 457 sentry.key =
461 458 sentry.public_key =
462 459 sentry.secret_key =
463 460 sentry.project =
464 461 sentry.site =
465 462 sentry.include_paths =
466 463 sentry.exclude_paths =
467 464
468 465 %endif
469 466 <%text>################################################################################</%text>
470 467 <%text>## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##</%text>
471 468 <%text>## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##</%text>
472 469 <%text>## execute malicious code after an exception is raised. ##</%text>
473 470 <%text>################################################################################</%text>
474 471 set debug = false
475 472
476 473 <%text>##################################</%text>
477 474 <%text>### LOGVIEW CONFIG ###</%text>
478 475 <%text>##################################</%text>
479 476
480 477 logview.sqlalchemy = #faa
481 478 logview.pylons.templating = #bfb
482 479 logview.pylons.util = #eee
483 480
484 481 <%text>#########################################################</%text>
485 482 <%text>### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###</%text>
486 483 <%text>#########################################################</%text>
487 484
488 485 %if database_engine == 'sqlite':
489 486 # SQLITE [default]
490 487 sqlalchemy.db1.url = sqlite:///${here}/kallithea.db?timeout=60
491 488
492 489 %elif database_engine == 'postgres':
493 490 # POSTGRESQL
494 491 sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
495 492
496 493 %elif database_engine == 'mysql':
497 494 # MySQL
498 495 sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea?charset=utf8
499 496
500 497 %endif
501 498 # see sqlalchemy docs for others
502 499
503 500 sqlalchemy.db1.echo = false
504 501 sqlalchemy.db1.pool_recycle = 3600
505 502
506 503 <%text>################################</%text>
507 504 <%text>### LOGGING CONFIGURATION ####</%text>
508 505 <%text>################################</%text>
509 506
510 507 [loggers]
511 508 keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
512 509
513 510 [handlers]
514 511 keys = console, console_sql
515 512
516 513 [formatters]
517 514 keys = generic, color_formatter, color_formatter_sql
518 515
519 516 <%text>#############</%text>
520 517 <%text>## LOGGERS ##</%text>
521 518 <%text>#############</%text>
522 519
523 520 [logger_root]
524 521 level = NOTSET
525 522 handlers = console
526 523
527 524 [logger_routes]
528 525 level = DEBUG
529 526 handlers =
530 527 qualname = routes.middleware
531 528 <%text>## "level = DEBUG" logs the route matched and routing variables.</%text>
532 529 propagate = 1
533 530
534 531 [logger_beaker]
535 532 level = DEBUG
536 533 handlers =
537 534 qualname = beaker.container
538 535 propagate = 1
539 536
540 537 [logger_templates]
541 538 level = INFO
542 539 handlers =
543 540 qualname = pylons.templating
544 541 propagate = 1
545 542
546 543 [logger_kallithea]
547 544 level = DEBUG
548 545 handlers =
549 546 qualname = kallithea
550 547 propagate = 1
551 548
552 549 [logger_sqlalchemy]
553 550 level = INFO
554 551 handlers = console_sql
555 552 qualname = sqlalchemy.engine
556 553 propagate = 0
557 554
558 555 [logger_whoosh_indexer]
559 556 level = DEBUG
560 557 handlers =
561 558 qualname = whoosh_indexer
562 559 propagate = 1
563 560
564 561 <%text>##############</%text>
565 562 <%text>## HANDLERS ##</%text>
566 563 <%text>##############</%text>
567 564
568 565 [handler_console]
569 566 class = StreamHandler
570 567 args = (sys.stderr,)
571 568 level = INFO
572 569 formatter = generic
573 570
574 571 [handler_console_sql]
575 572 class = StreamHandler
576 573 args = (sys.stderr,)
577 574 level = WARN
578 575 formatter = generic
579 576
580 577 <%text>################</%text>
581 578 <%text>## FORMATTERS ##</%text>
582 579 <%text>################</%text>
583 580
584 581 [formatter_generic]
585 582 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
586 583 datefmt = %Y-%m-%d %H:%M:%S
587 584
588 585 [formatter_color_formatter]
589 586 class = kallithea.lib.colored_formatter.ColorFormatter
590 587 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
591 588 datefmt = %Y-%m-%d %H:%M:%S
592 589
593 590 [formatter_color_formatter_sql]
594 591 class = kallithea.lib.colored_formatter.ColorFormatterSql
595 592 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
596 593 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,585 +1,582 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # Kallithea - Example 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 debug = true
11 11 pdebug = false
12 12
13 13 ################################################################################
14 14 ## Email settings ##
15 15 ## ##
16 16 ## Refer to the documentation ("Email settings") for more details. ##
17 17 ## ##
18 18 ## It is recommended to use a valid sender address that passes access ##
19 19 ## validation and spam filtering in mail servers. ##
20 20 ################################################################################
21 21
22 22 ## 'From' header for application emails. You can optionally add a name.
23 23 ## Default:
24 24 #app_email_from = Kallithea
25 25 ## Examples:
26 26 #app_email_from = Kallithea <kallithea-noreply@example.com>
27 27 #app_email_from = kallithea-noreply@example.com
28 28
29 29 ## Subject prefix for application emails.
30 30 ## A space between this prefix and the real subject is automatically added.
31 31 ## Default:
32 32 #email_prefix =
33 33 ## Example:
34 34 #email_prefix = [Kallithea]
35 35
36 36 ## Recipients for error emails and fallback recipients of application mails.
37 37 ## Multiple addresses can be specified, space-separated.
38 38 ## Only addresses are allowed, do not add any name part.
39 39 ## Default:
40 40 #email_to =
41 41 ## Examples:
42 42 #email_to = admin@example.com
43 43 #email_to = admin@example.com another_admin@example.com
44 44
45 45 ## 'From' header for error emails. You can optionally add a name.
46 46 ## Default:
47 47 #error_email_from = pylons@yourapp.com
48 48 ## Examples:
49 49 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
50 50 #error_email_from = paste_error@example.com
51 51
52 52 ## SMTP server settings
53 53 ## Only smtp_server is mandatory. All other settings take the specified default
54 54 ## values.
55 55 #smtp_server = smtp.example.com
56 56 #smtp_username =
57 57 #smtp_password =
58 58 #smtp_port = 25
59 59 #smtp_use_tls = false
60 60 #smtp_use_ssl = false
61 61 ## SMTP authentication parameters to use (e.g. LOGIN PLAIN CRAM-MD5, etc.).
62 62 ## If empty, use any of the authentication parameters supported by the server.
63 63 #smtp_auth =
64 64
65 65 [server:main]
66 66 ## PASTE ##
67 67 #use = egg:Paste#http
68 68 ## nr of worker threads to spawn
69 69 #threadpool_workers = 5
70 70 ## max request before thread respawn
71 71 #threadpool_max_requests = 10
72 72 ## option to use threads of process
73 73 #use_threadpool = true
74 74
75 75 ## WAITRESS ##
76 76 use = egg:waitress#main
77 77 ## number of worker threads
78 78 threads = 5
79 79 ## MAX BODY SIZE 100GB
80 80 max_request_body_size = 107374182400
81 81 ## use poll instead of select, fixes fd limits, may not work on old
82 82 ## windows systems.
83 83 #asyncore_use_poll = True
84 84
85 85 ## GUNICORN ##
86 86 #use = egg:gunicorn#main
87 87 ## number of process workers. You must set `instance_id = *` when this option
88 88 ## is set to more than one worker
89 89 #workers = 1
90 90 ## process name
91 91 #proc_name = kallithea
92 92 ## type of worker class, one of sync, eventlet, gevent, tornado
93 93 ## recommended for bigger setup is using of of other than sync one
94 94 #worker_class = sync
95 95 #max_requests = 1000
96 96 ## ammount of time a worker can handle request before it gets killed and
97 97 ## restarted
98 98 #timeout = 3600
99 99
100 100 ## UWSGI ##
101 101 ## run with uwsgi --ini-paste-logged <inifile.ini>
102 102 #[uwsgi]
103 103 #socket = /tmp/uwsgi.sock
104 104 #master = true
105 105 #http = 127.0.0.1:5000
106 106
107 107 ## set as deamon and redirect all output to file
108 108 #daemonize = ./uwsgi_kallithea.log
109 109
110 110 ## master process PID
111 111 #pidfile = ./uwsgi_kallithea.pid
112 112
113 113 ## stats server with workers statistics, use uwsgitop
114 114 ## for monitoring, `uwsgitop 127.0.0.1:1717`
115 115 #stats = 127.0.0.1:1717
116 116 #memory-report = true
117 117
118 118 ## log 5XX errors
119 119 #log-5xx = true
120 120
121 121 ## Set the socket listen queue size.
122 122 #listen = 256
123 123
124 124 ## Gracefully Reload workers after the specified amount of managed requests
125 125 ## (avoid memory leaks).
126 126 #max-requests = 1000
127 127
128 128 ## enable large buffers
129 129 #buffer-size = 65535
130 130
131 131 ## socket and http timeouts ##
132 132 #http-timeout = 3600
133 133 #socket-timeout = 3600
134 134
135 135 ## Log requests slower than the specified number of milliseconds.
136 136 #log-slow = 10
137 137
138 138 ## Exit if no app can be loaded.
139 139 #need-app = true
140 140
141 141 ## Set lazy mode (load apps in workers instead of master).
142 142 #lazy = true
143 143
144 144 ## scaling ##
145 145 ## set cheaper algorithm to use, if not set default will be used
146 146 #cheaper-algo = spare
147 147
148 148 ## minimum number of workers to keep at all times
149 149 #cheaper = 1
150 150
151 151 ## number of workers to spawn at startup
152 152 #cheaper-initial = 1
153 153
154 154 ## maximum number of workers that can be spawned
155 155 #workers = 4
156 156
157 157 ## how many workers should be spawned at a time
158 158 #cheaper-step = 1
159 159
160 160 ## COMMON ##
161 161 host = 127.0.0.1
162 162 port = 5000
163 163
164 164 ## middleware for hosting the WSGI application under a URL prefix
165 165 #[filter:proxy-prefix]
166 166 #use = egg:PasteDeploy#prefix
167 167 #prefix = /<your-prefix>
168 168
169 169 [app:main]
170 170 use = egg:kallithea
171 171 ## enable proxy prefix middleware
172 172 #filter-with = proxy-prefix
173 173
174 174 full_stack = true
175 175 static_files = true
176 176 ## Available Languages:
177 177 ## cs de fr hu ja nl_BE pl pt_BR ru sk zh_CN zh_TW
178 178 lang =
179 179 cache_dir = %(here)s/data
180 180 index_dir = %(here)s/data/index
181 181
182 182 ## perform a full repository scan on each server start, this should be
183 183 ## set to false after first startup, to allow faster server restarts.
184 184 initial_repo_scan = false
185 185
186 186 ## uncomment and set this path to use archive download cache
187 187 archive_cache_dir = %(here)s/tarballcache
188 188
189 189 ## change this to unique ID for security
190 190 app_instance_uuid = ${app_instance_uuid}
191 191
192 192 ## cut off limit for large diffs (size in bytes)
193 193 cut_off_limit = 256000
194 194
195 ## use cache version of scm repo everywhere
196 vcs_full_cache = true
197
198 195 ## force https in Kallithea, fixes https redirects, assumes it's always https
199 196 force_https = false
200 197
201 198 ## use Strict-Transport-Security headers
202 199 use_htsts = false
203 200
204 201 ## number of commits stats will parse on each iteration
205 202 commit_parse_limit = 25
206 203
207 204 ## path to git executable
208 205 git_path = git
209 206
210 207 ## git rev filter option, --all is the default filter, if you need to
211 208 ## hide all refs in changelog switch this to --branches --tags
212 209 #git_rev_filter = --branches --tags
213 210
214 211 ## RSS feed options
215 212 rss_cut_off_limit = 256000
216 213 rss_items_per_page = 10
217 214 rss_include_diff = false
218 215
219 216 ## options for showing and identifying changesets
220 217 show_sha_length = 12
221 218 show_revision_number = false
222 219
223 220 ## gist URL alias, used to create nicer urls for gist. This should be an
224 221 ## url that does rewrites to _admin/gists/<gistid>.
225 222 ## example: http://gist.example.com/{gistid}. Empty means use the internal
226 223 ## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid>
227 224 gist_alias_url =
228 225
229 226 ## white list of API enabled controllers. This allows to add list of
230 227 ## controllers to which access will be enabled by api_key. eg: to enable
231 228 ## api access to raw_files put `FilesController:raw`, to enable access to patches
232 229 ## add `ChangesetController:changeset_patch`. This list should be "," separated
233 230 ## Syntax is <ControllerClass>:<function>. Check debug logs for generated names
234 231 ## Recommended settings below are commented out:
235 232 api_access_controllers_whitelist =
236 233 # ChangesetController:changeset_patch,
237 234 # ChangesetController:changeset_raw,
238 235 # FilesController:raw,
239 236 # FilesController:archivefile
240 237
241 238 ## default encoding used to convert from and to unicode
242 239 ## can be also a comma seperated list of encoding in case of mixed encodings
243 240 default_encoding = utf8
244 241
245 242 ## issue tracker for Kallithea (leave blank to disable, absent for default)
246 243 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
247 244
248 245 ## issue tracking mapping for commits messages
249 246 ## comment out issue_pat, issue_server, issue_prefix to enable
250 247
251 248 ## pattern to get the issues from commit messages
252 249 ## default one used here is #<numbers> with a regex passive group for `#`
253 250 ## {id} will be all groups matched from this pattern
254 251
255 252 issue_pat = (?:\s*#)(\d+)
256 253
257 254 ## server url to the issue, each {id} will be replaced with match
258 255 ## fetched from the regex and {repo} is replaced with full repository name
259 256 ## including groups {repo_name} is replaced with just name of repo
260 257
261 258 issue_server_link = https://issues.example.com/{repo}/issue/{id}
262 259
263 260 ## prefix to add to link to indicate it's an url
264 261 ## #314 will be replaced by <issue_prefix><id>
265 262
266 263 issue_prefix = #
267 264
268 265 ## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
269 266 ## multiple patterns, to other issues server, wiki or others
270 267 ## below an example how to create a wiki pattern
271 268 # wiki-some-id -> https://wiki.example.com/some-id
272 269
273 270 #issue_pat_wiki = (?:wiki-)(.+)
274 271 #issue_server_link_wiki = https://wiki.example.com/{id}
275 272 #issue_prefix_wiki = WIKI-
276 273
277 274 ## alternative return HTTP header for failed authentication. Default HTTP
278 275 ## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
279 276 ## handling that. Set this variable to 403 to return HTTPForbidden
280 277 auth_ret_code =
281 278
282 279 ## locking return code. When repository is locked return this HTTP code. 2XX
283 280 ## codes don't break the transactions while 4XX codes do
284 281 lock_ret_code = 423
285 282
286 283 ## allows to change the repository location in settings page
287 284 allow_repo_location_change = True
288 285
289 286 ## allows to setup custom hooks in settings page
290 287 allow_custom_hooks_settings = True
291 288
292 289 ## extra extensions for indexing, space separated and without the leading '.'.
293 290 # index.extensions =
294 291 # gemfile
295 292 # lock
296 293
297 294 ## extra filenames for indexing, space separated
298 295 # index.filenames =
299 296 # .dockerignore
300 297 # .editorconfig
301 298 # INSTALL
302 299 # CHANGELOG
303 300
304 301 ####################################
305 302 ### CELERY CONFIG ####
306 303 ####################################
307 304
308 305 use_celery = false
309 306 broker.host = localhost
310 307 broker.vhost = rabbitmqhost
311 308 broker.port = 5672
312 309 broker.user = rabbitmq
313 310 broker.password = qweqwe
314 311
315 312 celery.imports = kallithea.lib.celerylib.tasks
316 313
317 314 celery.result.backend = amqp
318 315 celery.result.dburi = amqp://
319 316 celery.result.serialier = json
320 317
321 318 #celery.send.task.error.emails = true
322 319 #celery.amqp.task.result.expires = 18000
323 320
324 321 celeryd.concurrency = 2
325 322 #celeryd.log.file = celeryd.log
326 323 celeryd.log.level = DEBUG
327 324 celeryd.max.tasks.per.child = 1
328 325
329 326 ## tasks will never be sent to the queue, but executed locally instead.
330 327 celery.always.eager = false
331 328
332 329 ####################################
333 330 ### BEAKER CACHE ####
334 331 ####################################
335 332
336 333 beaker.cache.data_dir = %(here)s/data/cache/data
337 334 beaker.cache.lock_dir = %(here)s/data/cache/lock
338 335
339 336 beaker.cache.regions = short_term,long_term,sql_cache_short
340 337
341 338 beaker.cache.short_term.type = memory
342 339 beaker.cache.short_term.expire = 60
343 340 beaker.cache.short_term.key_length = 256
344 341
345 342 beaker.cache.long_term.type = memory
346 343 beaker.cache.long_term.expire = 36000
347 344 beaker.cache.long_term.key_length = 256
348 345
349 346 beaker.cache.sql_cache_short.type = memory
350 347 beaker.cache.sql_cache_short.expire = 10
351 348 beaker.cache.sql_cache_short.key_length = 256
352 349
353 350 ####################################
354 351 ### BEAKER SESSION ####
355 352 ####################################
356 353
357 354 ## Name of session cookie. Should be unique for a given host and path, even when running
358 355 ## on different ports. Otherwise, cookie sessions will be shared and messed up.
359 356 beaker.session.key = kallithea
360 357 ## Sessions should always only be accessible by the browser, not directly by JavaScript.
361 358 beaker.session.httponly = true
362 359 ## Session lifetime. 2592000 seconds is 30 days.
363 360 beaker.session.timeout = 2592000
364 361
365 362 ## Server secret used with HMAC to ensure integrity of cookies.
366 363 beaker.session.secret = ${app_instance_uuid}
367 364 ## Further, encrypt the data with AES.
368 365 #beaker.session.encrypt_key = <key_for_encryption>
369 366 #beaker.session.validate_key = <validation_key>
370 367
371 368 ## Type of storage used for the session, current types are
372 369 ## dbm, file, memcached, database, and memory.
373 370
374 371 ## File system storage of session data. (default)
375 372 #beaker.session.type = file
376 373
377 374 ## Cookie only, store all session data inside the cookie. Requires secure secrets.
378 375 #beaker.session.type = cookie
379 376
380 377 ## Database storage of session data.
381 378 #beaker.session.type = ext:database
382 379 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
383 380 #beaker.session.table_name = db_session
384 381
385 382 ############################
386 383 ## ERROR HANDLING SYSTEMS ##
387 384 ############################
388 385
389 386 ####################
390 387 ### [errormator] ###
391 388 ####################
392 389
393 390 ## Errormator is tailored to work with Kallithea, see
394 391 ## http://errormator.com for details how to obtain an account
395 392 ## you must install python package `errormator_client` to make it work
396 393
397 394 ## errormator enabled
398 395 errormator = false
399 396
400 397 errormator.server_url = https://api.errormator.com
401 398 errormator.api_key = YOUR_API_KEY
402 399
403 400 ## TWEAK AMOUNT OF INFO SENT HERE
404 401
405 402 ## enables 404 error logging (default False)
406 403 errormator.report_404 = false
407 404
408 405 ## time in seconds after request is considered being slow (default 1)
409 406 errormator.slow_request_time = 1
410 407
411 408 ## record slow requests in application
412 409 ## (needs to be enabled for slow datastore recording and time tracking)
413 410 errormator.slow_requests = true
414 411
415 412 ## enable hooking to application loggers
416 413 #errormator.logging = true
417 414
418 415 ## minimum log level for log capture
419 416 #errormator.logging.level = WARNING
420 417
421 418 ## send logs only from erroneous/slow requests
422 419 ## (saves API quota for intensive logging)
423 420 errormator.logging_on_error = false
424 421
425 422 ## list of additonal keywords that should be grabbed from environ object
426 423 ## can be string with comma separated list of words in lowercase
427 424 ## (by default client will always send following info:
428 425 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
429 426 ## start with HTTP* this list be extended with additional keywords here
430 427 errormator.environ_keys_whitelist =
431 428
432 429 ## list of keywords that should be blanked from request object
433 430 ## can be string with comma separated list of words in lowercase
434 431 ## (by default client will always blank keys that contain following words
435 432 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
436 433 ## this list be extended with additional keywords set here
437 434 errormator.request_keys_blacklist =
438 435
439 436 ## list of namespaces that should be ignores when gathering log entries
440 437 ## can be string with comma separated list of namespaces
441 438 ## (by default the client ignores own entries: errormator_client.client)
442 439 errormator.log_namespace_blacklist =
443 440
444 441 ################
445 442 ### [sentry] ###
446 443 ################
447 444
448 445 ## sentry is a alternative open source error aggregator
449 446 ## you must install python packages `sentry` and `raven` to enable
450 447
451 448 sentry.dsn = YOUR_DNS
452 449 sentry.servers =
453 450 sentry.name =
454 451 sentry.key =
455 452 sentry.public_key =
456 453 sentry.secret_key =
457 454 sentry.project =
458 455 sentry.site =
459 456 sentry.include_paths =
460 457 sentry.exclude_paths =
461 458
462 459 ################################################################################
463 460 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
464 461 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
465 462 ## execute malicious code after an exception is raised. ##
466 463 ################################################################################
467 464 set debug = false
468 465
469 466 ##################################
470 467 ### LOGVIEW CONFIG ###
471 468 ##################################
472 469
473 470 logview.sqlalchemy = #faa
474 471 logview.pylons.templating = #bfb
475 472 logview.pylons.util = #eee
476 473
477 474 #########################################################
478 475 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
479 476 #########################################################
480 477
481 478 # SQLITE [default]
482 479 sqlalchemy.db1.url = sqlite:///%(here)s/kallithea.db?timeout=60
483 480
484 481 # POSTGRESQL
485 482 #sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
486 483
487 484 # MySQL
488 485 #sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea?charset=utf8
489 486
490 487 # see sqlalchemy docs for others
491 488
492 489 sqlalchemy.db1.echo = false
493 490 sqlalchemy.db1.pool_recycle = 3600
494 491
495 492 ################################
496 493 ### LOGGING CONFIGURATION ####
497 494 ################################
498 495
499 496 [loggers]
500 497 keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
501 498
502 499 [handlers]
503 500 keys = console, console_sql
504 501
505 502 [formatters]
506 503 keys = generic, color_formatter, color_formatter_sql
507 504
508 505 #############
509 506 ## LOGGERS ##
510 507 #############
511 508
512 509 [logger_root]
513 510 level = NOTSET
514 511 handlers = console
515 512
516 513 [logger_routes]
517 514 level = DEBUG
518 515 handlers =
519 516 qualname = routes.middleware
520 517 ## "level = DEBUG" logs the route matched and routing variables.
521 518 propagate = 1
522 519
523 520 [logger_beaker]
524 521 level = DEBUG
525 522 handlers =
526 523 qualname = beaker.container
527 524 propagate = 1
528 525
529 526 [logger_templates]
530 527 level = INFO
531 528 handlers =
532 529 qualname = pylons.templating
533 530 propagate = 1
534 531
535 532 [logger_kallithea]
536 533 level = DEBUG
537 534 handlers =
538 535 qualname = kallithea
539 536 propagate = 1
540 537
541 538 [logger_sqlalchemy]
542 539 level = INFO
543 540 handlers = console_sql
544 541 qualname = sqlalchemy.engine
545 542 propagate = 0
546 543
547 544 [logger_whoosh_indexer]
548 545 level = DEBUG
549 546 handlers =
550 547 qualname = whoosh_indexer
551 548 propagate = 1
552 549
553 550 ##############
554 551 ## HANDLERS ##
555 552 ##############
556 553
557 554 [handler_console]
558 555 class = StreamHandler
559 556 args = (sys.stderr,)
560 557 level = INFO
561 558 formatter = generic
562 559
563 560 [handler_console_sql]
564 561 class = StreamHandler
565 562 args = (sys.stderr,)
566 563 level = WARN
567 564 formatter = generic
568 565
569 566 ################
570 567 ## FORMATTERS ##
571 568 ################
572 569
573 570 [formatter_generic]
574 571 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
575 572 datefmt = %Y-%m-%d %H:%M:%S
576 573
577 574 [formatter_color_formatter]
578 575 class = kallithea.lib.colored_formatter.ColorFormatter
579 576 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
580 577 datefmt = %Y-%m-%d %H:%M:%S
581 578
582 579 [formatter_color_formatter_sql]
583 580 class = kallithea.lib.colored_formatter.ColorFormatterSql
584 581 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
585 582 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,2570 +1,2566 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.model.db
16 16 ~~~~~~~~~~~~~~~~~~
17 17
18 18 Database Models 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 08, 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 time
30 30 import logging
31 31 import datetime
32 32 import traceback
33 33 import hashlib
34 34 import collections
35 35 import functools
36 36
37 37 import sqlalchemy
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.hybrid import hybrid_property
40 40 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
41 41 from beaker.cache import cache_region, region_invalidate
42 42 from webob.exc import HTTPNotFound
43 43
44 44 from pylons.i18n.translation import lazy_ugettext as _
45 45
46 46 from kallithea import DB_PREFIX
47 47 from kallithea.lib.exceptions import DefaultUserException
48 48 from kallithea.lib.vcs import get_backend
49 49 from kallithea.lib.vcs.utils.helpers import get_scm
50 50 from kallithea.lib.vcs.utils.lazy import LazyProperty
51 51 from kallithea.lib.vcs.backends.base import EmptyChangeset
52 52
53 53 from kallithea.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
54 54 safe_unicode, remove_prefix, time_to_datetime, aslist, Optional, safe_int, \
55 55 get_clone_url, urlreadable
56 56 from kallithea.lib.compat import json
57 57 from kallithea.lib.caching_query import FromCache
58 58
59 59 from kallithea.model.meta import Base, Session
60 60
61 61 URL_SEP = '/'
62 62 log = logging.getLogger(__name__)
63 63
64 64 #==============================================================================
65 65 # BASE CLASSES
66 66 #==============================================================================
67 67
68 68 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
69 69
70 70
71 71 class BaseModel(object):
72 72 """
73 73 Base Model for all classes
74 74 """
75 75
76 76 @classmethod
77 77 def _get_keys(cls):
78 78 """return column names for this model """
79 79 return class_mapper(cls).c.keys()
80 80
81 81 def get_dict(self):
82 82 """
83 83 return dict with keys and values corresponding
84 84 to this model data """
85 85
86 86 d = {}
87 87 for k in self._get_keys():
88 88 d[k] = getattr(self, k)
89 89
90 90 # also use __json__() if present to get additional fields
91 91 _json_attr = getattr(self, '__json__', None)
92 92 if _json_attr:
93 93 # update with attributes from __json__
94 94 if callable(_json_attr):
95 95 _json_attr = _json_attr()
96 96 for k, val in _json_attr.iteritems():
97 97 d[k] = val
98 98 return d
99 99
100 100 def get_appstruct(self):
101 101 """return list with keys and values tuples corresponding
102 102 to this model data """
103 103
104 104 l = []
105 105 for k in self._get_keys():
106 106 l.append((k, getattr(self, k),))
107 107 return l
108 108
109 109 def populate_obj(self, populate_dict):
110 110 """populate model with data from given populate_dict"""
111 111
112 112 for k in self._get_keys():
113 113 if k in populate_dict:
114 114 setattr(self, k, populate_dict[k])
115 115
116 116 @classmethod
117 117 def query(cls):
118 118 return Session().query(cls)
119 119
120 120 @classmethod
121 121 def get(cls, id_):
122 122 if id_:
123 123 return cls.query().get(id_)
124 124
125 125 @classmethod
126 126 def get_or_404(cls, id_):
127 127 try:
128 128 id_ = int(id_)
129 129 except (TypeError, ValueError):
130 130 raise HTTPNotFound
131 131
132 132 res = cls.query().get(id_)
133 133 if res is None:
134 134 raise HTTPNotFound
135 135 return res
136 136
137 137 @classmethod
138 138 def getAll(cls):
139 139 # deprecated and left for backward compatibility
140 140 return cls.get_all()
141 141
142 142 @classmethod
143 143 def get_all(cls):
144 144 return cls.query().all()
145 145
146 146 @classmethod
147 147 def delete(cls, id_):
148 148 obj = cls.query().get(id_)
149 149 Session().delete(obj)
150 150
151 151 def __repr__(self):
152 152 if hasattr(self, '__unicode__'):
153 153 # python repr needs to return str
154 154 try:
155 155 return safe_str(self.__unicode__())
156 156 except UnicodeDecodeError:
157 157 pass
158 158 return '<DB:%s>' % (self.__class__.__name__)
159 159
160 160
161 161 _table_args_default_dict = {'extend_existing': True,
162 162 'mysql_engine': 'InnoDB',
163 163 'mysql_charset': 'utf8',
164 164 'sqlite_autoincrement': True,
165 165 }
166 166
167 167 class Setting(Base, BaseModel):
168 168 __tablename__ = DB_PREFIX + 'settings'
169 169 __table_args__ = (
170 170 _table_args_default_dict,
171 171 )
172 172
173 173 SETTINGS_TYPES = {
174 174 'str': safe_str,
175 175 'int': safe_int,
176 176 'unicode': safe_unicode,
177 177 'bool': str2bool,
178 178 'list': functools.partial(aslist, sep=',')
179 179 }
180 180 DEFAULT_UPDATE_URL = ''
181 181
182 182 app_settings_id = Column(Integer(), unique=True, primary_key=True)
183 183 app_settings_name = Column(String(255), nullable=False, unique=True)
184 184 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
185 185 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
186 186
187 187 def __init__(self, key='', val='', type='unicode'):
188 188 self.app_settings_name = key
189 189 self.app_settings_value = val
190 190 self.app_settings_type = type
191 191
192 192 @validates('_app_settings_value')
193 193 def validate_settings_value(self, key, val):
194 194 assert type(val) == unicode
195 195 return val
196 196
197 197 @hybrid_property
198 198 def app_settings_value(self):
199 199 v = self._app_settings_value
200 200 _type = self.app_settings_type
201 201 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
202 202 return converter(v)
203 203
204 204 @app_settings_value.setter
205 205 def app_settings_value(self, val):
206 206 """
207 207 Setter that will always make sure we use unicode in app_settings_value
208 208
209 209 :param val:
210 210 """
211 211 self._app_settings_value = safe_unicode(val)
212 212
213 213 @hybrid_property
214 214 def app_settings_type(self):
215 215 return self._app_settings_type
216 216
217 217 @app_settings_type.setter
218 218 def app_settings_type(self, val):
219 219 if val not in self.SETTINGS_TYPES:
220 220 raise Exception('type must be one of %s got %s'
221 221 % (self.SETTINGS_TYPES.keys(), val))
222 222 self._app_settings_type = val
223 223
224 224 def __unicode__(self):
225 225 return u"<%s('%s:%s[%s]')>" % (
226 226 self.__class__.__name__,
227 227 self.app_settings_name, self.app_settings_value, self.app_settings_type
228 228 )
229 229
230 230 @classmethod
231 231 def get_by_name(cls, key):
232 232 return cls.query() \
233 233 .filter(cls.app_settings_name == key).scalar()
234 234
235 235 @classmethod
236 236 def get_by_name_or_create(cls, key, val='', type='unicode'):
237 237 res = cls.get_by_name(key)
238 238 if res is None:
239 239 res = cls(key, val, type)
240 240 return res
241 241
242 242 @classmethod
243 243 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
244 244 """
245 245 Creates or updates Kallithea setting. If updates are triggered, it will only
246 246 update parameters that are explicitly set. Optional instance will be skipped.
247 247
248 248 :param key:
249 249 :param val:
250 250 :param type:
251 251 :return:
252 252 """
253 253 res = cls.get_by_name(key)
254 254 if res is None:
255 255 val = Optional.extract(val)
256 256 type = Optional.extract(type)
257 257 res = cls(key, val, type)
258 258 else:
259 259 res.app_settings_name = key
260 260 if not isinstance(val, Optional):
261 261 # update if set
262 262 res.app_settings_value = val
263 263 if not isinstance(type, Optional):
264 264 # update if set
265 265 res.app_settings_type = type
266 266 return res
267 267
268 268 @classmethod
269 269 def get_app_settings(cls, cache=False):
270 270
271 271 ret = cls.query()
272 272
273 273 if cache:
274 274 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
275 275
276 276 if ret is None:
277 277 raise Exception('Could not get application settings !')
278 278 settings = {}
279 279 for each in ret:
280 280 settings[each.app_settings_name] = \
281 281 each.app_settings_value
282 282
283 283 return settings
284 284
285 285 @classmethod
286 286 def get_auth_plugins(cls, cache=False):
287 287 auth_plugins = cls.get_by_name("auth_plugins").app_settings_value
288 288 return auth_plugins
289 289
290 290 @classmethod
291 291 def get_auth_settings(cls, cache=False):
292 292 ret = cls.query() \
293 293 .filter(cls.app_settings_name.startswith('auth_')).all()
294 294 fd = {}
295 295 for row in ret:
296 296 fd[row.app_settings_name] = row.app_settings_value
297 297 return fd
298 298
299 299 @classmethod
300 300 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
301 301 ret = cls.query() \
302 302 .filter(cls.app_settings_name.startswith('default_')).all()
303 303 fd = {}
304 304 for row in ret:
305 305 key = row.app_settings_name
306 306 if strip_prefix:
307 307 key = remove_prefix(key, prefix='default_')
308 308 fd.update({key: row.app_settings_value})
309 309
310 310 return fd
311 311
312 312 @classmethod
313 313 def get_server_info(cls):
314 314 import pkg_resources
315 315 import platform
316 316 import kallithea
317 317 from kallithea.lib.utils import check_git_version
318 318 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
319 319 info = {
320 320 'modules': sorted(mods, key=lambda k: k[0].lower()),
321 321 'py_version': platform.python_version(),
322 322 'platform': safe_unicode(platform.platform()),
323 323 'kallithea_version': kallithea.__version__,
324 324 'git_version': safe_unicode(check_git_version()),
325 325 'git_path': kallithea.CONFIG.get('git_path')
326 326 }
327 327 return info
328 328
329 329
330 330 class Ui(Base, BaseModel):
331 331 __tablename__ = DB_PREFIX + 'ui'
332 332 __table_args__ = (
333 333 # FIXME: ui_key as key is wrong and should be removed when the corresponding
334 334 # Ui.get_by_key has been replaced by the composite key
335 335 UniqueConstraint('ui_key'),
336 336 UniqueConstraint('ui_section', 'ui_key'),
337 337 _table_args_default_dict,
338 338 )
339 339
340 340 HOOK_UPDATE = 'changegroup.update'
341 341 HOOK_REPO_SIZE = 'changegroup.repo_size'
342 342 HOOK_PUSH = 'changegroup.push_logger'
343 343 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
344 344 HOOK_PULL = 'outgoing.pull_logger'
345 345 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
346 346
347 347 ui_id = Column(Integer(), unique=True, primary_key=True)
348 348 ui_section = Column(String(255), nullable=False)
349 349 ui_key = Column(String(255), nullable=False)
350 350 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
351 351 ui_active = Column(Boolean(), nullable=False, default=True)
352 352
353 353 @classmethod
354 354 def get_by_key(cls, section, key):
355 355 """ Return specified Ui object, or None if not found. """
356 356 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
357 357
358 358 @classmethod
359 359 def get_or_create(cls, section, key):
360 360 """ Return specified Ui object, creating it if necessary. """
361 361 setting = cls.get_by_key(section, key)
362 362 if setting is None:
363 363 setting = cls(ui_section=section, ui_key=key)
364 364 Session().add(setting)
365 365 return setting
366 366
367 367 @classmethod
368 368 def get_builtin_hooks(cls):
369 369 q = cls.query()
370 370 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
371 371 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
372 372 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
373 373 q = q.filter(cls.ui_section == 'hooks')
374 374 return q.all()
375 375
376 376 @classmethod
377 377 def get_custom_hooks(cls):
378 378 q = cls.query()
379 379 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
380 380 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
381 381 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
382 382 q = q.filter(cls.ui_section == 'hooks')
383 383 return q.all()
384 384
385 385 @classmethod
386 386 def get_repos_location(cls):
387 387 return cls.get_by_key('paths', '/').ui_value
388 388
389 389 @classmethod
390 390 def create_or_update_hook(cls, key, val):
391 391 new_ui = cls.get_or_create('hooks', key)
392 392 new_ui.ui_active = True
393 393 new_ui.ui_value = val
394 394
395 395 def __repr__(self):
396 396 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
397 397 self.ui_key, self.ui_value)
398 398
399 399
400 400 class User(Base, BaseModel):
401 401 __tablename__ = 'users'
402 402 __table_args__ = (
403 403 Index('u_username_idx', 'username'),
404 404 Index('u_email_idx', 'email'),
405 405 _table_args_default_dict,
406 406 )
407 407
408 408 DEFAULT_USER = 'default'
409 409 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
410 410
411 411 user_id = Column(Integer(), unique=True, primary_key=True)
412 412 username = Column(String(255), nullable=False, unique=True)
413 413 password = Column(String(255), nullable=False)
414 414 active = Column(Boolean(), nullable=False, default=True)
415 415 admin = Column(Boolean(), nullable=False, default=False)
416 416 name = Column("firstname", Unicode(255), nullable=False)
417 417 lastname = Column(Unicode(255), nullable=False)
418 418 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
419 419 last_login = Column(DateTime(timezone=False), nullable=True)
420 420 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
421 421 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
422 422 api_key = Column(String(255), nullable=False)
423 423 inherit_default_permissions = Column(Boolean(), nullable=False, default=True)
424 424 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
425 425 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
426 426
427 427 user_log = relationship('UserLog')
428 428 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
429 429
430 430 repositories = relationship('Repository')
431 431 repo_groups = relationship('RepoGroup')
432 432 user_groups = relationship('UserGroup')
433 433 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
434 434 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
435 435
436 436 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
437 437 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
438 438
439 439 group_member = relationship('UserGroupMember', cascade='all')
440 440
441 441 notifications = relationship('UserNotification', cascade='all')
442 442 # notifications assigned to this user
443 443 user_created_notifications = relationship('Notification', cascade='all')
444 444 # comments created by this user
445 445 user_comments = relationship('ChangesetComment', cascade='all')
446 446 #extra emails for this user
447 447 user_emails = relationship('UserEmailMap', cascade='all')
448 448 #extra API keys
449 449 user_api_keys = relationship('UserApiKeys', cascade='all')
450 450
451 451
452 452 @hybrid_property
453 453 def email(self):
454 454 return self._email
455 455
456 456 @email.setter
457 457 def email(self, val):
458 458 self._email = val.lower() if val else None
459 459
460 460 @property
461 461 def firstname(self):
462 462 # alias for future
463 463 return self.name
464 464
465 465 @property
466 466 def emails(self):
467 467 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
468 468 return [self.email] + [x.email for x in other]
469 469
470 470 @property
471 471 def api_keys(self):
472 472 other = UserApiKeys.query().filter(UserApiKeys.user==self).all()
473 473 return [self.api_key] + [x.api_key for x in other]
474 474
475 475 @property
476 476 def ip_addresses(self):
477 477 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
478 478 return [x.ip_addr for x in ret]
479 479
480 480 @property
481 481 def full_name(self):
482 482 return '%s %s' % (self.firstname, self.lastname)
483 483
484 484 @property
485 485 def full_name_or_username(self):
486 486 """
487 487 Show full name.
488 488 If full name is not set, fall back to username.
489 489 """
490 490 return ('%s %s' % (self.firstname, self.lastname)
491 491 if (self.firstname and self.lastname) else self.username)
492 492
493 493 @property
494 494 def full_name_and_username(self):
495 495 """
496 496 Show full name and username as 'Firstname Lastname (username)'.
497 497 If full name is not set, fall back to username.
498 498 """
499 499 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
500 500 if (self.firstname and self.lastname) else self.username)
501 501
502 502 @property
503 503 def full_contact(self):
504 504 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
505 505
506 506 @property
507 507 def short_contact(self):
508 508 return '%s %s' % (self.firstname, self.lastname)
509 509
510 510 @property
511 511 def is_admin(self):
512 512 return self.admin
513 513
514 514 @property
515 515 def AuthUser(self):
516 516 """
517 517 Returns instance of AuthUser for this user
518 518 """
519 519 from kallithea.lib.auth import AuthUser
520 520 return AuthUser(dbuser=self)
521 521
522 522 @hybrid_property
523 523 def user_data(self):
524 524 if not self._user_data:
525 525 return {}
526 526
527 527 try:
528 528 return json.loads(self._user_data)
529 529 except TypeError:
530 530 return {}
531 531
532 532 @user_data.setter
533 533 def user_data(self, val):
534 534 try:
535 535 self._user_data = json.dumps(val)
536 536 except Exception:
537 537 log.error(traceback.format_exc())
538 538
539 539 def __unicode__(self):
540 540 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
541 541 self.user_id, self.username)
542 542
543 543 @classmethod
544 544 def get_or_404(cls, id_, allow_default=True):
545 545 '''
546 546 Overridden version of BaseModel.get_or_404, with an extra check on
547 547 the default user.
548 548 '''
549 549 user = super(User, cls).get_or_404(id_)
550 550 if allow_default == False:
551 551 if user.username == User.DEFAULT_USER:
552 552 raise DefaultUserException
553 553 return user
554 554
555 555 @classmethod
556 556 def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
557 557 """
558 558 For anything that looks like an email address, look up by the email address (matching
559 559 case insensitively).
560 560 For anything else, try to look up by the user name.
561 561
562 562 This assumes no normal username can have '@' symbol.
563 563 """
564 564 if '@' in username_or_email:
565 565 return User.get_by_email(username_or_email, cache=cache)
566 566 else:
567 567 return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
568 568
569 569 @classmethod
570 570 def get_by_username(cls, username, case_insensitive=False, cache=False):
571 571 if case_insensitive:
572 572 q = cls.query().filter(func.lower(cls.username) == func.lower(username))
573 573 else:
574 574 q = cls.query().filter(cls.username == username)
575 575
576 576 if cache:
577 577 q = q.options(FromCache(
578 578 "sql_cache_short",
579 579 "get_user_%s" % _hash_key(username)
580 580 )
581 581 )
582 582 return q.scalar()
583 583
584 584 @classmethod
585 585 def get_by_api_key(cls, api_key, cache=False, fallback=True):
586 586 if len(api_key) != 40 or not api_key.isalnum():
587 587 return None
588 588
589 589 q = cls.query().filter(cls.api_key == api_key)
590 590
591 591 if cache:
592 592 q = q.options(FromCache("sql_cache_short",
593 593 "get_api_key_%s" % api_key))
594 594 res = q.scalar()
595 595
596 596 if fallback and not res:
597 597 #fallback to additional keys
598 598 _res = UserApiKeys.query() \
599 599 .filter(UserApiKeys.api_key == api_key) \
600 600 .filter(or_(UserApiKeys.expires == -1,
601 601 UserApiKeys.expires >= time.time())) \
602 602 .first()
603 603 if _res:
604 604 res = _res.user
605 605 return res
606 606
607 607 @classmethod
608 608 def get_by_email(cls, email, cache=False):
609 609 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
610 610
611 611 if cache:
612 612 q = q.options(FromCache("sql_cache_short",
613 613 "get_email_key_%s" % email))
614 614
615 615 ret = q.scalar()
616 616 if ret is None:
617 617 q = UserEmailMap.query()
618 618 # try fetching in alternate email map
619 619 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
620 620 q = q.options(joinedload(UserEmailMap.user))
621 621 if cache:
622 622 q = q.options(FromCache("sql_cache_short",
623 623 "get_email_map_key_%s" % email))
624 624 ret = getattr(q.scalar(), 'user', None)
625 625
626 626 return ret
627 627
628 628 @classmethod
629 629 def get_from_cs_author(cls, author):
630 630 """
631 631 Tries to get User objects out of commit author string
632 632
633 633 :param author:
634 634 """
635 635 from kallithea.lib.helpers import email, author_name
636 636 # Valid email in the attribute passed, see if they're in the system
637 637 _email = email(author)
638 638 if _email:
639 639 user = cls.get_by_email(_email)
640 640 if user is not None:
641 641 return user
642 642 # Maybe we can match by username?
643 643 _author = author_name(author)
644 644 user = cls.get_by_username(_author, case_insensitive=True)
645 645 if user is not None:
646 646 return user
647 647
648 648 def update_lastlogin(self):
649 649 """Update user lastlogin"""
650 650 self.last_login = datetime.datetime.now()
651 651 Session().add(self)
652 652 log.debug('updated user %s lastlogin', self.username)
653 653
654 654 @classmethod
655 655 def get_first_admin(cls):
656 656 user = User.query().filter(User.admin == True).first()
657 657 if user is None:
658 658 raise Exception('Missing administrative account!')
659 659 return user
660 660
661 661 @classmethod
662 662 def get_default_user(cls, cache=False):
663 663 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
664 664 if user is None:
665 665 raise Exception('Missing default account!')
666 666 return user
667 667
668 668 def get_api_data(self, details=False):
669 669 """
670 670 Common function for generating user related data for API
671 671 """
672 672 user = self
673 673 data = dict(
674 674 user_id=user.user_id,
675 675 username=user.username,
676 676 firstname=user.name,
677 677 lastname=user.lastname,
678 678 email=user.email,
679 679 emails=user.emails,
680 680 active=user.active,
681 681 admin=user.admin,
682 682 )
683 683 if details:
684 684 data.update(dict(
685 685 extern_type=user.extern_type,
686 686 extern_name=user.extern_name,
687 687 api_key=user.api_key,
688 688 api_keys=user.api_keys,
689 689 last_login=user.last_login,
690 690 ip_addresses=user.ip_addresses
691 691 ))
692 692 return data
693 693
694 694 def __json__(self):
695 695 data = dict(
696 696 full_name=self.full_name,
697 697 full_name_or_username=self.full_name_or_username,
698 698 short_contact=self.short_contact,
699 699 full_contact=self.full_contact
700 700 )
701 701 data.update(self.get_api_data())
702 702 return data
703 703
704 704
705 705 class UserApiKeys(Base, BaseModel):
706 706 __tablename__ = 'user_api_keys'
707 707 __table_args__ = (
708 708 Index('uak_api_key_idx', 'api_key'),
709 709 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
710 710 _table_args_default_dict,
711 711 )
712 712 __mapper_args__ = {}
713 713
714 714 user_api_key_id = Column(Integer(), unique=True, primary_key=True)
715 715 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
716 716 api_key = Column(String(255), nullable=False, unique=True)
717 717 description = Column(UnicodeText(1024), nullable=False)
718 718 expires = Column(Float(53), nullable=False)
719 719 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
720 720
721 721 user = relationship('User')
722 722
723 723 @property
724 724 def expired(self):
725 725 if self.expires == -1:
726 726 return False
727 727 return time.time() > self.expires
728 728
729 729
730 730 class UserEmailMap(Base, BaseModel):
731 731 __tablename__ = 'user_email_map'
732 732 __table_args__ = (
733 733 Index('uem_email_idx', 'email'),
734 734 _table_args_default_dict,
735 735 )
736 736 __mapper_args__ = {}
737 737
738 738 email_id = Column(Integer(), unique=True, primary_key=True)
739 739 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
740 740 _email = Column("email", String(255), nullable=False, unique=True)
741 741 user = relationship('User')
742 742
743 743 @validates('_email')
744 744 def validate_email(self, key, email):
745 745 # check if this email is not main one
746 746 main_email = Session().query(User).filter(User.email == email).scalar()
747 747 if main_email is not None:
748 748 raise AttributeError('email %s is present is user table' % email)
749 749 return email
750 750
751 751 @hybrid_property
752 752 def email(self):
753 753 return self._email
754 754
755 755 @email.setter
756 756 def email(self, val):
757 757 self._email = val.lower() if val else None
758 758
759 759
760 760 class UserIpMap(Base, BaseModel):
761 761 __tablename__ = 'user_ip_map'
762 762 __table_args__ = (
763 763 UniqueConstraint('user_id', 'ip_addr'),
764 764 _table_args_default_dict,
765 765 )
766 766 __mapper_args__ = {}
767 767
768 768 ip_id = Column(Integer(), unique=True, primary_key=True)
769 769 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
770 770 ip_addr = Column(String(255), nullable=False)
771 771 active = Column(Boolean(), nullable=False, default=True)
772 772 user = relationship('User')
773 773
774 774 @classmethod
775 775 def _get_ip_range(cls, ip_addr):
776 776 from kallithea.lib import ipaddr
777 777 net = ipaddr.IPNetwork(address=ip_addr)
778 778 return [str(net.network), str(net.broadcast)]
779 779
780 780 def __json__(self):
781 781 return dict(
782 782 ip_addr=self.ip_addr,
783 783 ip_range=self._get_ip_range(self.ip_addr)
784 784 )
785 785
786 786 def __unicode__(self):
787 787 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
788 788 self.user_id, self.ip_addr)
789 789
790 790 class UserLog(Base, BaseModel):
791 791 __tablename__ = 'user_logs'
792 792 __table_args__ = (
793 793 _table_args_default_dict,
794 794 )
795 795
796 796 user_log_id = Column(Integer(), unique=True, primary_key=True)
797 797 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
798 798 username = Column(String(255), nullable=False)
799 799 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
800 800 repository_name = Column(Unicode(255), nullable=False)
801 801 user_ip = Column(String(255), nullable=True)
802 802 action = Column(UnicodeText(1200000), nullable=False)
803 803 action_date = Column(DateTime(timezone=False), nullable=False)
804 804
805 805 def __unicode__(self):
806 806 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
807 807 self.repository_name,
808 808 self.action)
809 809
810 810 @property
811 811 def action_as_day(self):
812 812 return datetime.date(*self.action_date.timetuple()[:3])
813 813
814 814 user = relationship('User')
815 815 repository = relationship('Repository', cascade='')
816 816
817 817
818 818 class UserGroup(Base, BaseModel):
819 819 __tablename__ = 'users_groups'
820 820 __table_args__ = (
821 821 _table_args_default_dict,
822 822 )
823 823
824 824 users_group_id = Column(Integer(), unique=True, primary_key=True)
825 825 users_group_name = Column(Unicode(255), nullable=False, unique=True)
826 826 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
827 827 users_group_active = Column(Boolean(), nullable=False)
828 828 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, default=True)
829 829 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
830 830 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
831 831 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
832 832
833 833 members = relationship('UserGroupMember', cascade="all, delete-orphan")
834 834 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
835 835 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
836 836 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
837 837 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
838 838 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
839 839
840 840 user = relationship('User')
841 841
842 842 @hybrid_property
843 843 def group_data(self):
844 844 if not self._group_data:
845 845 return {}
846 846
847 847 try:
848 848 return json.loads(self._group_data)
849 849 except TypeError:
850 850 return {}
851 851
852 852 @group_data.setter
853 853 def group_data(self, val):
854 854 try:
855 855 self._group_data = json.dumps(val)
856 856 except Exception:
857 857 log.error(traceback.format_exc())
858 858
859 859 def __unicode__(self):
860 860 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
861 861 self.users_group_id,
862 862 self.users_group_name)
863 863
864 864 @classmethod
865 865 def get_by_group_name(cls, group_name, cache=False,
866 866 case_insensitive=False):
867 867 if case_insensitive:
868 868 q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
869 869 else:
870 870 q = cls.query().filter(cls.users_group_name == group_name)
871 871 if cache:
872 872 q = q.options(FromCache(
873 873 "sql_cache_short",
874 874 "get_group_%s" % _hash_key(group_name)
875 875 )
876 876 )
877 877 return q.scalar()
878 878
879 879 @classmethod
880 880 def get(cls, user_group_id, cache=False):
881 881 user_group = cls.query()
882 882 if cache:
883 883 user_group = user_group.options(FromCache("sql_cache_short",
884 884 "get_users_group_%s" % user_group_id))
885 885 return user_group.get(user_group_id)
886 886
887 887 def get_api_data(self, with_members=True):
888 888 user_group = self
889 889
890 890 data = dict(
891 891 users_group_id=user_group.users_group_id,
892 892 group_name=user_group.users_group_name,
893 893 group_description=user_group.user_group_description,
894 894 active=user_group.users_group_active,
895 895 owner=user_group.user.username,
896 896 )
897 897 if with_members:
898 898 members = []
899 899 for user in user_group.members:
900 900 user = user.user
901 901 members.append(user.get_api_data())
902 902 data['members'] = members
903 903
904 904 return data
905 905
906 906
907 907 class UserGroupMember(Base, BaseModel):
908 908 __tablename__ = 'users_groups_members'
909 909 __table_args__ = (
910 910 _table_args_default_dict,
911 911 )
912 912
913 913 users_group_member_id = Column(Integer(), unique=True, primary_key=True)
914 914 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
915 915 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
916 916
917 917 user = relationship('User')
918 918 users_group = relationship('UserGroup')
919 919
920 920 def __init__(self, gr_id='', u_id=''):
921 921 self.users_group_id = gr_id
922 922 self.user_id = u_id
923 923
924 924
925 925 class RepositoryField(Base, BaseModel):
926 926 __tablename__ = 'repositories_fields'
927 927 __table_args__ = (
928 928 UniqueConstraint('repository_id', 'field_key'), # no-multi field
929 929 _table_args_default_dict,
930 930 )
931 931
932 932 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
933 933
934 934 repo_field_id = Column(Integer(), unique=True, primary_key=True)
935 935 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
936 936 field_key = Column(String(250), nullable=False)
937 937 field_label = Column(String(1024), nullable=False)
938 938 field_value = Column(String(10000), nullable=False)
939 939 field_desc = Column(String(1024), nullable=False)
940 940 field_type = Column(String(255), nullable=False)
941 941 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
942 942
943 943 repository = relationship('Repository')
944 944
945 945 @property
946 946 def field_key_prefixed(self):
947 947 return 'ex_%s' % self.field_key
948 948
949 949 @classmethod
950 950 def un_prefix_key(cls, key):
951 951 if key.startswith(cls.PREFIX):
952 952 return key[len(cls.PREFIX):]
953 953 return key
954 954
955 955 @classmethod
956 956 def get_by_key_name(cls, key, repo):
957 957 row = cls.query() \
958 958 .filter(cls.repository == repo) \
959 959 .filter(cls.field_key == key).scalar()
960 960 return row
961 961
962 962
963 963 class Repository(Base, BaseModel):
964 964 __tablename__ = 'repositories'
965 965 __table_args__ = (
966 966 Index('r_repo_name_idx', 'repo_name'),
967 967 _table_args_default_dict,
968 968 )
969 969
970 970 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
971 971 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
972 972
973 973 STATE_CREATED = 'repo_state_created'
974 974 STATE_PENDING = 'repo_state_pending'
975 975 STATE_ERROR = 'repo_state_error'
976 976
977 977 repo_id = Column(Integer(), unique=True, primary_key=True)
978 978 repo_name = Column(Unicode(255), nullable=False, unique=True)
979 979 repo_state = Column(String(255), nullable=False)
980 980
981 981 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
982 982 repo_type = Column(String(255), nullable=False)
983 983 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
984 984 private = Column(Boolean(), nullable=False)
985 985 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
986 986 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
987 987 description = Column(Unicode(10000), nullable=False)
988 988 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
989 989 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
990 990 _landing_revision = Column("landing_revision", String(255), nullable=False)
991 991 enable_locking = Column(Boolean(), nullable=False, default=False)
992 992 _locked = Column("locked", String(255), nullable=True) # FIXME: not nullable?
993 993 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
994 994
995 995 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
996 996 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
997 997
998 998 user = relationship('User')
999 999 fork = relationship('Repository', remote_side=repo_id)
1000 1000 group = relationship('RepoGroup')
1001 1001 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1002 1002 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1003 1003 stats = relationship('Statistics', cascade='all', uselist=False)
1004 1004
1005 1005 followers = relationship('UserFollowing',
1006 1006 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1007 1007 cascade='all')
1008 1008 extra_fields = relationship('RepositoryField',
1009 1009 cascade="all, delete-orphan")
1010 1010
1011 1011 logs = relationship('UserLog')
1012 1012 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
1013 1013
1014 1014 pull_requests_org = relationship('PullRequest',
1015 1015 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
1016 1016 cascade="all, delete-orphan")
1017 1017
1018 1018 pull_requests_other = relationship('PullRequest',
1019 1019 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
1020 1020 cascade="all, delete-orphan")
1021 1021
1022 1022 def __unicode__(self):
1023 1023 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1024 1024 safe_unicode(self.repo_name))
1025 1025
1026 1026 @hybrid_property
1027 1027 def landing_rev(self):
1028 1028 # always should return [rev_type, rev]
1029 1029 if self._landing_revision:
1030 1030 _rev_info = self._landing_revision.split(':')
1031 1031 if len(_rev_info) < 2:
1032 1032 _rev_info.insert(0, 'rev')
1033 1033 return [_rev_info[0], _rev_info[1]]
1034 1034 return [None, None]
1035 1035
1036 1036 @landing_rev.setter
1037 1037 def landing_rev(self, val):
1038 1038 if ':' not in val:
1039 1039 raise ValueError('value must be delimited with `:` and consist '
1040 1040 'of <rev_type>:<rev>, got %s instead' % val)
1041 1041 self._landing_revision = val
1042 1042
1043 1043 @hybrid_property
1044 1044 def locked(self):
1045 1045 # always should return [user_id, timelocked]
1046 1046 if self._locked:
1047 1047 _lock_info = self._locked.split(':')
1048 1048 return int(_lock_info[0]), _lock_info[1]
1049 1049 return [None, None]
1050 1050
1051 1051 @locked.setter
1052 1052 def locked(self, val):
1053 1053 if val and isinstance(val, (list, tuple)):
1054 1054 self._locked = ':'.join(map(str, val))
1055 1055 else:
1056 1056 self._locked = None
1057 1057
1058 1058 @hybrid_property
1059 1059 def changeset_cache(self):
1060 1060 try:
1061 1061 cs_cache = json.loads(self._changeset_cache) # might raise on bad data
1062 1062 cs_cache['raw_id'] # verify data, raise exception on error
1063 1063 return cs_cache
1064 1064 except (TypeError, KeyError, ValueError):
1065 1065 return EmptyChangeset().__json__()
1066 1066
1067 1067 @changeset_cache.setter
1068 1068 def changeset_cache(self, val):
1069 1069 try:
1070 1070 self._changeset_cache = json.dumps(val)
1071 1071 except Exception:
1072 1072 log.error(traceback.format_exc())
1073 1073
1074 1074 @classmethod
1075 1075 def url_sep(cls):
1076 1076 return URL_SEP
1077 1077
1078 1078 @classmethod
1079 1079 def normalize_repo_name(cls, repo_name):
1080 1080 """
1081 1081 Normalizes os specific repo_name to the format internally stored inside
1082 1082 database using URL_SEP
1083 1083
1084 1084 :param cls:
1085 1085 :param repo_name:
1086 1086 """
1087 1087 return cls.url_sep().join(repo_name.split(os.sep))
1088 1088
1089 1089 @classmethod
1090 1090 def get_by_repo_name(cls, repo_name):
1091 1091 q = Session().query(cls).filter(cls.repo_name == repo_name)
1092 1092 q = q.options(joinedload(Repository.fork)) \
1093 1093 .options(joinedload(Repository.user)) \
1094 1094 .options(joinedload(Repository.group))
1095 1095 return q.scalar()
1096 1096
1097 1097 @classmethod
1098 1098 def get_by_full_path(cls, repo_full_path):
1099 1099 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1100 1100 repo_name = cls.normalize_repo_name(repo_name)
1101 1101 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1102 1102
1103 1103 @classmethod
1104 1104 def get_repo_forks(cls, repo_id):
1105 1105 return cls.query().filter(Repository.fork_id == repo_id)
1106 1106
1107 1107 @classmethod
1108 1108 def base_path(cls):
1109 1109 """
1110 1110 Returns base path where all repos are stored
1111 1111
1112 1112 :param cls:
1113 1113 """
1114 1114 q = Session().query(Ui) \
1115 1115 .filter(Ui.ui_key == cls.url_sep())
1116 1116 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1117 1117 return q.one().ui_value
1118 1118
1119 1119 @property
1120 1120 def forks(self):
1121 1121 """
1122 1122 Return forks of this repo
1123 1123 """
1124 1124 return Repository.get_repo_forks(self.repo_id)
1125 1125
1126 1126 @property
1127 1127 def parent(self):
1128 1128 """
1129 1129 Returns fork parent
1130 1130 """
1131 1131 return self.fork
1132 1132
1133 1133 @property
1134 1134 def just_name(self):
1135 1135 return self.repo_name.split(Repository.url_sep())[-1]
1136 1136
1137 1137 @property
1138 1138 def groups_with_parents(self):
1139 1139 groups = []
1140 1140 if self.group is None:
1141 1141 return groups
1142 1142
1143 1143 cur_gr = self.group
1144 1144 groups.insert(0, cur_gr)
1145 1145 while 1:
1146 1146 gr = getattr(cur_gr, 'parent_group', None)
1147 1147 cur_gr = cur_gr.parent_group
1148 1148 if gr is None:
1149 1149 break
1150 1150 groups.insert(0, gr)
1151 1151
1152 1152 return groups
1153 1153
1154 1154 @property
1155 1155 def groups_and_repo(self):
1156 1156 return self.groups_with_parents, self.just_name, self.repo_name
1157 1157
1158 1158 @LazyProperty
1159 1159 def repo_path(self):
1160 1160 """
1161 1161 Returns base full path for that repository means where it actually
1162 1162 exists on a filesystem
1163 1163 """
1164 1164 q = Session().query(Ui).filter(Ui.ui_key ==
1165 1165 Repository.url_sep())
1166 1166 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1167 1167 return q.one().ui_value
1168 1168
1169 1169 @property
1170 1170 def repo_full_path(self):
1171 1171 p = [self.repo_path]
1172 1172 # we need to split the name by / since this is how we store the
1173 1173 # names in the database, but that eventually needs to be converted
1174 1174 # into a valid system path
1175 1175 p += self.repo_name.split(Repository.url_sep())
1176 1176 return os.path.join(*map(safe_unicode, p))
1177 1177
1178 1178 @property
1179 1179 def cache_keys(self):
1180 1180 """
1181 1181 Returns associated cache keys for that repo
1182 1182 """
1183 1183 return CacheInvalidation.query() \
1184 1184 .filter(CacheInvalidation.cache_args == self.repo_name) \
1185 1185 .order_by(CacheInvalidation.cache_key) \
1186 1186 .all()
1187 1187
1188 1188 def get_new_name(self, repo_name):
1189 1189 """
1190 1190 returns new full repository name based on assigned group and new new
1191 1191
1192 1192 :param group_name:
1193 1193 """
1194 1194 path_prefix = self.group.full_path_splitted if self.group else []
1195 1195 return Repository.url_sep().join(path_prefix + [repo_name])
1196 1196
1197 1197 @property
1198 1198 def _ui(self):
1199 1199 """
1200 1200 Creates an db based ui object for this repository
1201 1201 """
1202 1202 from kallithea.lib.utils import make_ui
1203 1203 return make_ui('db', clear_session=False)
1204 1204
1205 1205 @classmethod
1206 1206 def is_valid(cls, repo_name):
1207 1207 """
1208 1208 returns True if given repo name is a valid filesystem repository
1209 1209
1210 1210 :param cls:
1211 1211 :param repo_name:
1212 1212 """
1213 1213 from kallithea.lib.utils import is_valid_repo
1214 1214
1215 1215 return is_valid_repo(repo_name, cls.base_path())
1216 1216
1217 1217 def get_api_data(self):
1218 1218 """
1219 1219 Common function for generating repo api data
1220 1220
1221 1221 """
1222 1222 repo = self
1223 1223 data = dict(
1224 1224 repo_id=repo.repo_id,
1225 1225 repo_name=repo.repo_name,
1226 1226 repo_type=repo.repo_type,
1227 1227 clone_uri=repo.clone_uri,
1228 1228 private=repo.private,
1229 1229 created_on=repo.created_on,
1230 1230 description=repo.description,
1231 1231 landing_rev=repo.landing_rev,
1232 1232 owner=repo.user.username,
1233 1233 fork_of=repo.fork.repo_name if repo.fork else None,
1234 1234 enable_statistics=repo.enable_statistics,
1235 1235 enable_locking=repo.enable_locking,
1236 1236 enable_downloads=repo.enable_downloads,
1237 1237 last_changeset=repo.changeset_cache,
1238 1238 locked_by=User.get(self.locked[0]).get_api_data() \
1239 1239 if self.locked[0] else None,
1240 1240 locked_date=time_to_datetime(self.locked[1]) \
1241 1241 if self.locked[1] else None
1242 1242 )
1243 1243 rc_config = Setting.get_app_settings()
1244 1244 repository_fields = str2bool(rc_config.get('repository_fields'))
1245 1245 if repository_fields:
1246 1246 for f in self.extra_fields:
1247 1247 data[f.field_key_prefixed] = f.field_value
1248 1248
1249 1249 return data
1250 1250
1251 1251 @classmethod
1252 1252 def lock(cls, repo, user_id, lock_time=None):
1253 1253 if lock_time is not None:
1254 1254 lock_time = time.time()
1255 1255 repo.locked = [user_id, lock_time]
1256 1256 Session().add(repo)
1257 1257 Session().commit()
1258 1258
1259 1259 @classmethod
1260 1260 def unlock(cls, repo):
1261 1261 repo.locked = None
1262 1262 Session().add(repo)
1263 1263 Session().commit()
1264 1264
1265 1265 @classmethod
1266 1266 def getlock(cls, repo):
1267 1267 return repo.locked
1268 1268
1269 1269 @property
1270 1270 def last_db_change(self):
1271 1271 return self.updated_on
1272 1272
1273 1273 @property
1274 1274 def clone_uri_hidden(self):
1275 1275 clone_uri = self.clone_uri
1276 1276 if clone_uri:
1277 1277 import urlobject
1278 1278 url_obj = urlobject.URLObject(self.clone_uri)
1279 1279 if url_obj.password:
1280 1280 clone_uri = url_obj.with_password('*****')
1281 1281 return clone_uri
1282 1282
1283 1283 def clone_url(self, **override):
1284 1284 import kallithea.lib.helpers as h
1285 1285 qualified_home_url = h.canonical_url('home')
1286 1286
1287 1287 uri_tmpl = None
1288 1288 if 'with_id' in override:
1289 1289 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1290 1290 del override['with_id']
1291 1291
1292 1292 if 'uri_tmpl' in override:
1293 1293 uri_tmpl = override['uri_tmpl']
1294 1294 del override['uri_tmpl']
1295 1295
1296 1296 # we didn't override our tmpl from **overrides
1297 1297 if not uri_tmpl:
1298 1298 uri_tmpl = self.DEFAULT_CLONE_URI
1299 1299 try:
1300 1300 from pylons import tmpl_context as c
1301 1301 uri_tmpl = c.clone_uri_tmpl
1302 1302 except AttributeError:
1303 1303 # in any case if we call this outside of request context,
1304 1304 # ie, not having tmpl_context set up
1305 1305 pass
1306 1306
1307 1307 return get_clone_url(uri_tmpl=uri_tmpl,
1308 1308 qualified_home_url=qualified_home_url,
1309 1309 repo_name=self.repo_name,
1310 1310 repo_id=self.repo_id, **override)
1311 1311
1312 1312 def set_state(self, state):
1313 1313 self.repo_state = state
1314 1314 Session().add(self)
1315 1315 #==========================================================================
1316 1316 # SCM PROPERTIES
1317 1317 #==========================================================================
1318 1318
1319 1319 def get_changeset(self, rev=None):
1320 1320 return get_changeset_safe(self.scm_instance, rev)
1321 1321
1322 1322 def get_landing_changeset(self):
1323 1323 """
1324 1324 Returns landing changeset, or if that doesn't exist returns the tip
1325 1325 """
1326 1326 _rev_type, _rev = self.landing_rev
1327 1327 cs = self.get_changeset(_rev)
1328 1328 if isinstance(cs, EmptyChangeset):
1329 1329 return self.get_changeset()
1330 1330 return cs
1331 1331
1332 1332 def update_changeset_cache(self, cs_cache=None):
1333 1333 """
1334 1334 Update cache of last changeset for repository, keys should be::
1335 1335
1336 1336 short_id
1337 1337 raw_id
1338 1338 revision
1339 1339 message
1340 1340 date
1341 1341 author
1342 1342
1343 1343 :param cs_cache:
1344 1344 """
1345 1345 from kallithea.lib.vcs.backends.base import BaseChangeset
1346 1346 if cs_cache is None:
1347 1347 cs_cache = EmptyChangeset()
1348 1348 # use no-cache version here
1349 1349 scm_repo = self.scm_instance_no_cache()
1350 1350 if scm_repo:
1351 1351 cs_cache = scm_repo.get_changeset()
1352 1352
1353 1353 if isinstance(cs_cache, BaseChangeset):
1354 1354 cs_cache = cs_cache.__json__()
1355 1355
1356 1356 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1357 1357 _default = datetime.datetime.fromtimestamp(0)
1358 1358 last_change = cs_cache.get('date') or _default
1359 1359 log.debug('updated repo %s with new cs cache %s',
1360 1360 self.repo_name, cs_cache)
1361 1361 self.updated_on = last_change
1362 1362 self.changeset_cache = cs_cache
1363 1363 Session().add(self)
1364 1364 Session().commit()
1365 1365 else:
1366 1366 log.debug('changeset_cache for %s already up to date with %s',
1367 1367 self.repo_name, cs_cache['raw_id'])
1368 1368
1369 1369 @property
1370 1370 def tip(self):
1371 1371 return self.get_changeset('tip')
1372 1372
1373 1373 @property
1374 1374 def author(self):
1375 1375 return self.tip.author
1376 1376
1377 1377 @property
1378 1378 def last_change(self):
1379 1379 return self.scm_instance.last_change
1380 1380
1381 1381 def get_comments(self, revisions=None):
1382 1382 """
1383 1383 Returns comments for this repository grouped by revisions
1384 1384
1385 1385 :param revisions: filter query by revisions only
1386 1386 """
1387 1387 cmts = ChangesetComment.query() \
1388 1388 .filter(ChangesetComment.repo == self)
1389 1389 if revisions is not None:
1390 1390 if not revisions:
1391 1391 return {} # don't use sql 'in' on empty set
1392 1392 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1393 1393 grouped = collections.defaultdict(list)
1394 1394 for cmt in cmts.all():
1395 1395 grouped[cmt.revision].append(cmt)
1396 1396 return grouped
1397 1397
1398 1398 def statuses(self, revisions):
1399 1399 """
1400 1400 Returns statuses for this repository.
1401 1401 PRs without any votes do _not_ show up as unreviewed.
1402 1402
1403 1403 :param revisions: list of revisions to get statuses for
1404 1404 """
1405 1405 if not revisions:
1406 1406 return {}
1407 1407
1408 1408 statuses = ChangesetStatus.query() \
1409 1409 .filter(ChangesetStatus.repo == self) \
1410 1410 .filter(ChangesetStatus.version == 0) \
1411 1411 .filter(ChangesetStatus.revision.in_(revisions))
1412 1412
1413 1413 grouped = {}
1414 1414 for stat in statuses.all():
1415 1415 pr_id = pr_nice_id = pr_repo = None
1416 1416 if stat.pull_request:
1417 1417 pr_id = stat.pull_request.pull_request_id
1418 1418 pr_nice_id = PullRequest.make_nice_id(pr_id)
1419 1419 pr_repo = stat.pull_request.other_repo.repo_name
1420 1420 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1421 1421 pr_id, pr_repo, pr_nice_id,
1422 1422 stat.author]
1423 1423 return grouped
1424 1424
1425 1425 def _repo_size(self):
1426 1426 from kallithea.lib import helpers as h
1427 1427 log.debug('calculating repository size...')
1428 1428 return h.format_byte_size(self.scm_instance.size)
1429 1429
1430 1430 #==========================================================================
1431 1431 # SCM CACHE INSTANCE
1432 1432 #==========================================================================
1433 1433
1434 1434 def set_invalidate(self):
1435 1435 """
1436 1436 Mark caches of this repo as invalid.
1437 1437 """
1438 1438 CacheInvalidation.set_invalidate(self.repo_name)
1439 1439
1440 1440 @property
1441 1441 def scm_instance(self):
1442 import kallithea
1443 full_cache = str2bool(kallithea.CONFIG.get('vcs_full_cache'))
1444 if full_cache:
1445 return self.scm_instance_cached()
1446 return self.__get_instance()
1442 return self.scm_instance_cached()
1447 1443
1448 1444 def scm_instance_cached(self, valid_cache_keys=None):
1449 1445 @cache_region('long_term', 'scm_instance_cached')
1450 1446 def _c(repo_name): # repo_name is just for the cache key
1451 1447 log.debug('Creating new %s scm_instance and populating cache', repo_name)
1452 1448 return self.scm_instance_no_cache()
1453 1449 rn = self.repo_name
1454 1450
1455 1451 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1456 1452 if not valid:
1457 1453 log.debug('Cache for %s invalidated, getting new object', rn)
1458 1454 region_invalidate(_c, None, 'scm_instance_cached', rn)
1459 1455 else:
1460 1456 log.debug('Trying to get scm_instance of %s from cache', rn)
1461 1457 return _c(rn)
1462 1458
1463 1459 def scm_instance_no_cache(self):
1464 1460 repo_full_path = safe_str(self.repo_full_path)
1465 1461 alias = get_scm(repo_full_path)[0]
1466 1462 log.debug('Creating instance of %s repository from %s',
1467 1463 alias, self.repo_full_path)
1468 1464 backend = get_backend(alias)
1469 1465
1470 1466 if alias == 'hg':
1471 1467 repo = backend(repo_full_path, create=False,
1472 1468 baseui=self._ui)
1473 1469 else:
1474 1470 repo = backend(repo_full_path, create=False)
1475 1471
1476 1472 return repo
1477 1473
1478 1474 def __json__(self):
1479 1475 return dict(landing_rev = self.landing_rev)
1480 1476
1481 1477 class RepoGroup(Base, BaseModel):
1482 1478 __tablename__ = 'groups'
1483 1479 __table_args__ = (
1484 1480 UniqueConstraint('group_name', 'group_parent_id'),
1485 1481 CheckConstraint('group_id != group_parent_id'),
1486 1482 _table_args_default_dict,
1487 1483 )
1488 1484 __mapper_args__ = {'order_by': 'group_name'}
1489 1485
1490 1486 SEP = ' &raquo; '
1491 1487
1492 1488 group_id = Column(Integer(), unique=True, primary_key=True)
1493 1489 group_name = Column(Unicode(255), nullable=False, unique=True)
1494 1490 group_parent_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
1495 1491 group_description = Column(Unicode(10000), nullable=False)
1496 1492 enable_locking = Column(Boolean(), nullable=False, default=False)
1497 1493 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1498 1494 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1499 1495
1500 1496 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1501 1497 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1502 1498 parent_group = relationship('RepoGroup', remote_side=group_id)
1503 1499 user = relationship('User')
1504 1500
1505 1501 def __init__(self, group_name='', parent_group=None):
1506 1502 self.group_name = group_name
1507 1503 self.parent_group = parent_group
1508 1504
1509 1505 def __unicode__(self):
1510 1506 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1511 1507 self.group_name)
1512 1508
1513 1509 @classmethod
1514 1510 def _generate_choice(cls, repo_group):
1515 1511 """Return tuple with group_id and name as html literal"""
1516 1512 from webhelpers.html import literal
1517 1513 if repo_group is None:
1518 1514 return (-1, u'-- %s --' % _('top level'))
1519 1515 return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
1520 1516
1521 1517 @classmethod
1522 1518 def groups_choices(cls, groups):
1523 1519 """Return tuples with group_id and name as html literal."""
1524 1520 return sorted((cls._generate_choice(g) for g in groups),
1525 1521 key=lambda c: c[1].split(cls.SEP))
1526 1522
1527 1523 @classmethod
1528 1524 def url_sep(cls):
1529 1525 return URL_SEP
1530 1526
1531 1527 @classmethod
1532 1528 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1533 1529 if case_insensitive:
1534 1530 gr = cls.query() \
1535 1531 .filter(func.lower(cls.group_name) == func.lower(group_name))
1536 1532 else:
1537 1533 gr = cls.query() \
1538 1534 .filter(cls.group_name == group_name)
1539 1535 if cache:
1540 1536 gr = gr.options(FromCache(
1541 1537 "sql_cache_short",
1542 1538 "get_group_%s" % _hash_key(group_name)
1543 1539 )
1544 1540 )
1545 1541 return gr.scalar()
1546 1542
1547 1543 @property
1548 1544 def parents(self):
1549 1545 parents_recursion_limit = 10
1550 1546 groups = []
1551 1547 if self.parent_group is None:
1552 1548 return groups
1553 1549 cur_gr = self.parent_group
1554 1550 groups.insert(0, cur_gr)
1555 1551 cnt = 0
1556 1552 while 1:
1557 1553 cnt += 1
1558 1554 gr = getattr(cur_gr, 'parent_group', None)
1559 1555 cur_gr = cur_gr.parent_group
1560 1556 if gr is None:
1561 1557 break
1562 1558 if cnt == parents_recursion_limit:
1563 1559 # this will prevent accidental infinite loops
1564 1560 log.error(('more than %s parents found for group %s, stopping '
1565 1561 'recursive parent fetching' % (parents_recursion_limit, self)))
1566 1562 break
1567 1563
1568 1564 groups.insert(0, gr)
1569 1565 return groups
1570 1566
1571 1567 @property
1572 1568 def children(self):
1573 1569 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1574 1570
1575 1571 @property
1576 1572 def name(self):
1577 1573 return self.group_name.split(RepoGroup.url_sep())[-1]
1578 1574
1579 1575 @property
1580 1576 def full_path(self):
1581 1577 return self.group_name
1582 1578
1583 1579 @property
1584 1580 def full_path_splitted(self):
1585 1581 return self.group_name.split(RepoGroup.url_sep())
1586 1582
1587 1583 @property
1588 1584 def repositories(self):
1589 1585 return Repository.query() \
1590 1586 .filter(Repository.group == self) \
1591 1587 .order_by(Repository.repo_name)
1592 1588
1593 1589 @property
1594 1590 def repositories_recursive_count(self):
1595 1591 cnt = self.repositories.count()
1596 1592
1597 1593 def children_count(group):
1598 1594 cnt = 0
1599 1595 for child in group.children:
1600 1596 cnt += child.repositories.count()
1601 1597 cnt += children_count(child)
1602 1598 return cnt
1603 1599
1604 1600 return cnt + children_count(self)
1605 1601
1606 1602 def _recursive_objects(self, include_repos=True):
1607 1603 all_ = []
1608 1604
1609 1605 def _get_members(root_gr):
1610 1606 if include_repos:
1611 1607 for r in root_gr.repositories:
1612 1608 all_.append(r)
1613 1609 childs = root_gr.children.all()
1614 1610 if childs:
1615 1611 for gr in childs:
1616 1612 all_.append(gr)
1617 1613 _get_members(gr)
1618 1614
1619 1615 _get_members(self)
1620 1616 return [self] + all_
1621 1617
1622 1618 def recursive_groups_and_repos(self):
1623 1619 """
1624 1620 Recursive return all groups, with repositories in those groups
1625 1621 """
1626 1622 return self._recursive_objects()
1627 1623
1628 1624 def recursive_groups(self):
1629 1625 """
1630 1626 Returns all children groups for this group including children of children
1631 1627 """
1632 1628 return self._recursive_objects(include_repos=False)
1633 1629
1634 1630 def get_new_name(self, group_name):
1635 1631 """
1636 1632 returns new full group name based on parent and new name
1637 1633
1638 1634 :param group_name:
1639 1635 """
1640 1636 path_prefix = (self.parent_group.full_path_splitted if
1641 1637 self.parent_group else [])
1642 1638 return RepoGroup.url_sep().join(path_prefix + [group_name])
1643 1639
1644 1640 def get_api_data(self):
1645 1641 """
1646 1642 Common function for generating api data
1647 1643
1648 1644 """
1649 1645 group = self
1650 1646 data = dict(
1651 1647 group_id=group.group_id,
1652 1648 group_name=group.group_name,
1653 1649 group_description=group.group_description,
1654 1650 parent_group=group.parent_group.group_name if group.parent_group else None,
1655 1651 repositories=[x.repo_name for x in group.repositories],
1656 1652 owner=group.user.username
1657 1653 )
1658 1654 return data
1659 1655
1660 1656
1661 1657 class Permission(Base, BaseModel):
1662 1658 __tablename__ = 'permissions'
1663 1659 __table_args__ = (
1664 1660 Index('p_perm_name_idx', 'permission_name'),
1665 1661 _table_args_default_dict,
1666 1662 )
1667 1663
1668 1664 PERMS = [
1669 1665 ('hg.admin', _('Kallithea Administrator')),
1670 1666
1671 1667 ('repository.none', _('Default user has no access to new repositories')),
1672 1668 ('repository.read', _('Default user has read access to new repositories')),
1673 1669 ('repository.write', _('Default user has write access to new repositories')),
1674 1670 ('repository.admin', _('Default user has admin access to new repositories')),
1675 1671
1676 1672 ('group.none', _('Default user has no access to new repository groups')),
1677 1673 ('group.read', _('Default user has read access to new repository groups')),
1678 1674 ('group.write', _('Default user has write access to new repository groups')),
1679 1675 ('group.admin', _('Default user has admin access to new repository groups')),
1680 1676
1681 1677 ('usergroup.none', _('Default user has no access to new user groups')),
1682 1678 ('usergroup.read', _('Default user has read access to new user groups')),
1683 1679 ('usergroup.write', _('Default user has write access to new user groups')),
1684 1680 ('usergroup.admin', _('Default user has admin access to new user groups')),
1685 1681
1686 1682 ('hg.repogroup.create.false', _('Only admins can create repository groups')),
1687 1683 ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
1688 1684
1689 1685 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1690 1686 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1691 1687
1692 1688 ('hg.create.none', _('Only admins can create top level repositories')),
1693 1689 ('hg.create.repository', _('Non-admins can create top level repositories')),
1694 1690
1695 1691 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1696 1692 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1697 1693
1698 1694 ('hg.fork.none', _('Only admins can fork repositories')),
1699 1695 ('hg.fork.repository', _('Non-admins can fork repositories')),
1700 1696
1701 1697 ('hg.register.none', _('Registration disabled')),
1702 1698 ('hg.register.manual_activate', _('User registration with manual account activation')),
1703 1699 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1704 1700
1705 1701 ('hg.extern_activate.manual', _('Manual activation of external account')),
1706 1702 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1707 1703 ]
1708 1704
1709 1705 #definition of system default permissions for DEFAULT user
1710 1706 DEFAULT_USER_PERMISSIONS = [
1711 1707 'repository.read',
1712 1708 'group.read',
1713 1709 'usergroup.read',
1714 1710 'hg.create.repository',
1715 1711 'hg.create.write_on_repogroup.true',
1716 1712 'hg.fork.repository',
1717 1713 'hg.register.manual_activate',
1718 1714 'hg.extern_activate.auto',
1719 1715 ]
1720 1716
1721 1717 # defines which permissions are more important higher the more important
1722 1718 # Weight defines which permissions are more important.
1723 1719 # The higher number the more important.
1724 1720 PERM_WEIGHTS = {
1725 1721 'repository.none': 0,
1726 1722 'repository.read': 1,
1727 1723 'repository.write': 3,
1728 1724 'repository.admin': 4,
1729 1725
1730 1726 'group.none': 0,
1731 1727 'group.read': 1,
1732 1728 'group.write': 3,
1733 1729 'group.admin': 4,
1734 1730
1735 1731 'usergroup.none': 0,
1736 1732 'usergroup.read': 1,
1737 1733 'usergroup.write': 3,
1738 1734 'usergroup.admin': 4,
1739 1735
1740 1736 'hg.repogroup.create.false': 0,
1741 1737 'hg.repogroup.create.true': 1,
1742 1738
1743 1739 'hg.usergroup.create.false': 0,
1744 1740 'hg.usergroup.create.true': 1,
1745 1741
1746 1742 'hg.fork.none': 0,
1747 1743 'hg.fork.repository': 1,
1748 1744
1749 1745 'hg.create.none': 0,
1750 1746 'hg.create.repository': 1
1751 1747 }
1752 1748
1753 1749 permission_id = Column(Integer(), unique=True, primary_key=True)
1754 1750 permission_name = Column(String(255), nullable=False)
1755 1751
1756 1752 def __unicode__(self):
1757 1753 return u"<%s('%s:%s')>" % (
1758 1754 self.__class__.__name__, self.permission_id, self.permission_name
1759 1755 )
1760 1756
1761 1757 @classmethod
1762 1758 def get_by_key(cls, key):
1763 1759 return cls.query().filter(cls.permission_name == key).scalar()
1764 1760
1765 1761 @classmethod
1766 1762 def get_default_perms(cls, default_user_id):
1767 1763 q = Session().query(UserRepoToPerm, Repository, cls) \
1768 1764 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
1769 1765 .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
1770 1766 .filter(UserRepoToPerm.user_id == default_user_id)
1771 1767
1772 1768 return q.all()
1773 1769
1774 1770 @classmethod
1775 1771 def get_default_group_perms(cls, default_user_id):
1776 1772 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
1777 1773 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
1778 1774 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
1779 1775 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1780 1776
1781 1777 return q.all()
1782 1778
1783 1779 @classmethod
1784 1780 def get_default_user_group_perms(cls, default_user_id):
1785 1781 q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
1786 1782 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
1787 1783 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
1788 1784 .filter(UserUserGroupToPerm.user_id == default_user_id)
1789 1785
1790 1786 return q.all()
1791 1787
1792 1788
1793 1789 class UserRepoToPerm(Base, BaseModel):
1794 1790 __tablename__ = 'repo_to_perm'
1795 1791 __table_args__ = (
1796 1792 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1797 1793 _table_args_default_dict,
1798 1794 )
1799 1795
1800 1796 repo_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1801 1797 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1802 1798 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1803 1799 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1804 1800
1805 1801 user = relationship('User')
1806 1802 repository = relationship('Repository')
1807 1803 permission = relationship('Permission')
1808 1804
1809 1805 @classmethod
1810 1806 def create(cls, user, repository, permission):
1811 1807 n = cls()
1812 1808 n.user = user
1813 1809 n.repository = repository
1814 1810 n.permission = permission
1815 1811 Session().add(n)
1816 1812 return n
1817 1813
1818 1814 def __unicode__(self):
1819 1815 return u'<%s => %s >' % (self.user, self.repository)
1820 1816
1821 1817
1822 1818 class UserUserGroupToPerm(Base, BaseModel):
1823 1819 __tablename__ = 'user_user_group_to_perm'
1824 1820 __table_args__ = (
1825 1821 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1826 1822 _table_args_default_dict,
1827 1823 )
1828 1824
1829 1825 user_user_group_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1830 1826 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1831 1827 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1832 1828 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1833 1829
1834 1830 user = relationship('User')
1835 1831 user_group = relationship('UserGroup')
1836 1832 permission = relationship('Permission')
1837 1833
1838 1834 @classmethod
1839 1835 def create(cls, user, user_group, permission):
1840 1836 n = cls()
1841 1837 n.user = user
1842 1838 n.user_group = user_group
1843 1839 n.permission = permission
1844 1840 Session().add(n)
1845 1841 return n
1846 1842
1847 1843 def __unicode__(self):
1848 1844 return u'<%s => %s >' % (self.user, self.user_group)
1849 1845
1850 1846
1851 1847 class UserToPerm(Base, BaseModel):
1852 1848 __tablename__ = 'user_to_perm'
1853 1849 __table_args__ = (
1854 1850 UniqueConstraint('user_id', 'permission_id'),
1855 1851 _table_args_default_dict,
1856 1852 )
1857 1853
1858 1854 user_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1859 1855 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1860 1856 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1861 1857
1862 1858 user = relationship('User')
1863 1859 permission = relationship('Permission')
1864 1860
1865 1861 def __unicode__(self):
1866 1862 return u'<%s => %s >' % (self.user, self.permission)
1867 1863
1868 1864
1869 1865 class UserGroupRepoToPerm(Base, BaseModel):
1870 1866 __tablename__ = 'users_group_repo_to_perm'
1871 1867 __table_args__ = (
1872 1868 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1873 1869 _table_args_default_dict,
1874 1870 )
1875 1871
1876 1872 users_group_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1877 1873 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1878 1874 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1879 1875 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1880 1876
1881 1877 users_group = relationship('UserGroup')
1882 1878 permission = relationship('Permission')
1883 1879 repository = relationship('Repository')
1884 1880
1885 1881 @classmethod
1886 1882 def create(cls, users_group, repository, permission):
1887 1883 n = cls()
1888 1884 n.users_group = users_group
1889 1885 n.repository = repository
1890 1886 n.permission = permission
1891 1887 Session().add(n)
1892 1888 return n
1893 1889
1894 1890 def __unicode__(self):
1895 1891 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1896 1892
1897 1893
1898 1894 class UserGroupUserGroupToPerm(Base, BaseModel):
1899 1895 __tablename__ = 'user_group_user_group_to_perm'
1900 1896 __table_args__ = (
1901 1897 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1902 1898 CheckConstraint('target_user_group_id != user_group_id'),
1903 1899 _table_args_default_dict,
1904 1900 )
1905 1901
1906 1902 user_group_user_group_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1907 1903 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1908 1904 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1909 1905 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1910 1906
1911 1907 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1912 1908 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1913 1909 permission = relationship('Permission')
1914 1910
1915 1911 @classmethod
1916 1912 def create(cls, target_user_group, user_group, permission):
1917 1913 n = cls()
1918 1914 n.target_user_group = target_user_group
1919 1915 n.user_group = user_group
1920 1916 n.permission = permission
1921 1917 Session().add(n)
1922 1918 return n
1923 1919
1924 1920 def __unicode__(self):
1925 1921 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1926 1922
1927 1923
1928 1924 class UserGroupToPerm(Base, BaseModel):
1929 1925 __tablename__ = 'users_group_to_perm'
1930 1926 __table_args__ = (
1931 1927 UniqueConstraint('users_group_id', 'permission_id',),
1932 1928 _table_args_default_dict,
1933 1929 )
1934 1930
1935 1931 users_group_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1936 1932 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1937 1933 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1938 1934
1939 1935 users_group = relationship('UserGroup')
1940 1936 permission = relationship('Permission')
1941 1937
1942 1938
1943 1939 class UserRepoGroupToPerm(Base, BaseModel):
1944 1940 __tablename__ = 'user_repo_group_to_perm'
1945 1941 __table_args__ = (
1946 1942 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1947 1943 _table_args_default_dict,
1948 1944 )
1949 1945
1950 1946 group_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1951 1947 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1952 1948 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1953 1949 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1954 1950
1955 1951 user = relationship('User')
1956 1952 group = relationship('RepoGroup')
1957 1953 permission = relationship('Permission')
1958 1954
1959 1955 @classmethod
1960 1956 def create(cls, user, repository_group, permission):
1961 1957 n = cls()
1962 1958 n.user = user
1963 1959 n.group = repository_group
1964 1960 n.permission = permission
1965 1961 Session().add(n)
1966 1962 return n
1967 1963
1968 1964
1969 1965 class UserGroupRepoGroupToPerm(Base, BaseModel):
1970 1966 __tablename__ = 'users_group_repo_group_to_perm'
1971 1967 __table_args__ = (
1972 1968 UniqueConstraint('users_group_id', 'group_id'),
1973 1969 _table_args_default_dict,
1974 1970 )
1975 1971
1976 1972 users_group_repo_group_to_perm_id = Column(Integer(), unique=True, primary_key=True)
1977 1973 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1978 1974 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1979 1975 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1980 1976
1981 1977 users_group = relationship('UserGroup')
1982 1978 permission = relationship('Permission')
1983 1979 group = relationship('RepoGroup')
1984 1980
1985 1981 @classmethod
1986 1982 def create(cls, user_group, repository_group, permission):
1987 1983 n = cls()
1988 1984 n.users_group = user_group
1989 1985 n.group = repository_group
1990 1986 n.permission = permission
1991 1987 Session().add(n)
1992 1988 return n
1993 1989
1994 1990
1995 1991 class Statistics(Base, BaseModel):
1996 1992 __tablename__ = 'statistics'
1997 1993 __table_args__ = (
1998 1994 _table_args_default_dict,
1999 1995 )
2000 1996
2001 1997 stat_id = Column(Integer(), unique=True, primary_key=True)
2002 1998 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
2003 1999 stat_on_revision = Column(Integer(), nullable=False)
2004 2000 commit_activity = Column(LargeBinary(1000000), nullable=False)#JSON data
2005 2001 commit_activity_combined = Column(LargeBinary(), nullable=False)#JSON data
2006 2002 languages = Column(LargeBinary(1000000), nullable=False)#JSON data
2007 2003
2008 2004 repository = relationship('Repository', single_parent=True)
2009 2005
2010 2006
2011 2007 class UserFollowing(Base, BaseModel):
2012 2008 __tablename__ = 'user_followings'
2013 2009 __table_args__ = (
2014 2010 UniqueConstraint('user_id', 'follows_repository_id'),
2015 2011 UniqueConstraint('user_id', 'follows_user_id'),
2016 2012 _table_args_default_dict,
2017 2013 )
2018 2014
2019 2015 user_following_id = Column(Integer(), unique=True, primary_key=True)
2020 2016 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2021 2017 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
2022 2018 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
2023 2019 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2024 2020
2025 2021 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2026 2022
2027 2023 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2028 2024 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2029 2025
2030 2026 @classmethod
2031 2027 def get_repo_followers(cls, repo_id):
2032 2028 return cls.query().filter(cls.follows_repo_id == repo_id)
2033 2029
2034 2030
2035 2031 class CacheInvalidation(Base, BaseModel):
2036 2032 __tablename__ = 'cache_invalidation'
2037 2033 __table_args__ = (
2038 2034 Index('key_idx', 'cache_key'),
2039 2035 _table_args_default_dict,
2040 2036 )
2041 2037
2042 2038 # cache_id, not used
2043 2039 cache_id = Column(Integer(), unique=True, primary_key=True)
2044 2040 # cache_key as created by _get_cache_key
2045 2041 cache_key = Column(Unicode(255), nullable=False, unique=True)
2046 2042 # cache_args is a repo_name
2047 2043 cache_args = Column(Unicode(255), nullable=False)
2048 2044 # instance sets cache_active True when it is caching, other instances set
2049 2045 # cache_active to False to indicate that this cache is invalid
2050 2046 cache_active = Column(Boolean(), nullable=False, default=False)
2051 2047
2052 2048 def __init__(self, cache_key, repo_name=''):
2053 2049 self.cache_key = cache_key
2054 2050 self.cache_args = repo_name
2055 2051 self.cache_active = False
2056 2052
2057 2053 def __unicode__(self):
2058 2054 return u"<%s('%s:%s[%s]')>" % (
2059 2055 self.__class__.__name__,
2060 2056 self.cache_id, self.cache_key, self.cache_active)
2061 2057
2062 2058 def _cache_key_partition(self):
2063 2059 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2064 2060 return prefix, repo_name, suffix
2065 2061
2066 2062 def get_prefix(self):
2067 2063 """
2068 2064 get prefix that might have been used in _get_cache_key to
2069 2065 generate self.cache_key. Only used for informational purposes
2070 2066 in repo_edit.html.
2071 2067 """
2072 2068 # prefix, repo_name, suffix
2073 2069 return self._cache_key_partition()[0]
2074 2070
2075 2071 def get_suffix(self):
2076 2072 """
2077 2073 get suffix that might have been used in _get_cache_key to
2078 2074 generate self.cache_key. Only used for informational purposes
2079 2075 in repo_edit.html.
2080 2076 """
2081 2077 # prefix, repo_name, suffix
2082 2078 return self._cache_key_partition()[2]
2083 2079
2084 2080 @classmethod
2085 2081 def clear_cache(cls):
2086 2082 """
2087 2083 Delete all cache keys from database.
2088 2084 Should only be run when all instances are down and all entries thus stale.
2089 2085 """
2090 2086 cls.query().delete()
2091 2087 Session().commit()
2092 2088
2093 2089 @classmethod
2094 2090 def _get_cache_key(cls, key):
2095 2091 """
2096 2092 Wrapper for generating a unique cache key for this instance and "key".
2097 2093 key must / will start with a repo_name which will be stored in .cache_args .
2098 2094 """
2099 2095 import kallithea
2100 2096 prefix = kallithea.CONFIG.get('instance_id', '')
2101 2097 return "%s%s" % (prefix, key)
2102 2098
2103 2099 @classmethod
2104 2100 def set_invalidate(cls, repo_name):
2105 2101 """
2106 2102 Mark all caches of a repo as invalid in the database.
2107 2103 """
2108 2104 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2109 2105 log.debug('for repo %s got %s invalidation objects',
2110 2106 safe_str(repo_name), inv_objs)
2111 2107
2112 2108 for inv_obj in inv_objs:
2113 2109 log.debug('marking %s key for invalidation based on repo_name=%s',
2114 2110 inv_obj, safe_str(repo_name))
2115 2111 Session().delete(inv_obj)
2116 2112 Session().commit()
2117 2113
2118 2114 @classmethod
2119 2115 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2120 2116 """
2121 2117 Mark this cache key as active and currently cached.
2122 2118 Return True if the existing cache registration still was valid.
2123 2119 Return False to indicate that it had been invalidated and caches should be refreshed.
2124 2120 """
2125 2121
2126 2122 key = (repo_name + '_' + kind) if kind else repo_name
2127 2123 cache_key = cls._get_cache_key(key)
2128 2124
2129 2125 if valid_cache_keys and cache_key in valid_cache_keys:
2130 2126 return True
2131 2127
2132 2128 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2133 2129 if inv_obj is None:
2134 2130 inv_obj = cls(cache_key, repo_name)
2135 2131 elif inv_obj.cache_active:
2136 2132 return True
2137 2133 inv_obj.cache_active = True
2138 2134 Session().add(inv_obj)
2139 2135 try:
2140 2136 Session().commit()
2141 2137 except sqlalchemy.exc.IntegrityError:
2142 2138 log.error('commit of CacheInvalidation failed - retrying')
2143 2139 Session().rollback()
2144 2140 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2145 2141 if inv_obj is None:
2146 2142 log.error('failed to create CacheInvalidation entry')
2147 2143 # TODO: fail badly?
2148 2144 # else: TOCTOU - another thread added the key at the same time; no further action required
2149 2145 return False
2150 2146
2151 2147 @classmethod
2152 2148 def get_valid_cache_keys(cls):
2153 2149 """
2154 2150 Return opaque object with information of which caches still are valid
2155 2151 and can be used without checking for invalidation.
2156 2152 """
2157 2153 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2158 2154
2159 2155
2160 2156 class ChangesetComment(Base, BaseModel):
2161 2157 __tablename__ = 'changeset_comments'
2162 2158 __table_args__ = (
2163 2159 Index('cc_revision_idx', 'revision'),
2164 2160 Index('cc_pull_request_id_idx', 'pull_request_id'),
2165 2161 _table_args_default_dict,
2166 2162 )
2167 2163
2168 2164 comment_id = Column(Integer(), unique=True, primary_key=True)
2169 2165 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2170 2166 revision = Column(String(40), nullable=True)
2171 2167 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2172 2168 line_no = Column(Unicode(10), nullable=True)
2173 2169 f_path = Column(Unicode(1000), nullable=True)
2174 2170 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2175 2171 text = Column(UnicodeText(25000), nullable=False)
2176 2172 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2177 2173 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2178 2174
2179 2175 author = relationship('User')
2180 2176 repo = relationship('Repository')
2181 2177 # status_change is frequently used directly in templates - make it a lazy
2182 2178 # join to avoid fetching each related ChangesetStatus on demand.
2183 2179 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2184 2180 status_change = relationship('ChangesetStatus',
2185 2181 cascade="all, delete-orphan", lazy='joined')
2186 2182 pull_request = relationship('PullRequest')
2187 2183
2188 2184 @classmethod
2189 2185 def get_users(cls, revision=None, pull_request_id=None):
2190 2186 """
2191 2187 Returns user associated with this ChangesetComment. ie those
2192 2188 who actually commented
2193 2189
2194 2190 :param cls:
2195 2191 :param revision:
2196 2192 """
2197 2193 q = Session().query(User) \
2198 2194 .join(ChangesetComment.author)
2199 2195 if revision is not None:
2200 2196 q = q.filter(cls.revision == revision)
2201 2197 elif pull_request_id is not None:
2202 2198 q = q.filter(cls.pull_request_id == pull_request_id)
2203 2199 return q.all()
2204 2200
2205 2201 def url(self):
2206 2202 anchor = "comment-%s" % self.comment_id
2207 2203 import kallithea.lib.helpers as h
2208 2204 if self.revision:
2209 2205 return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
2210 2206 elif self.pull_request_id is not None:
2211 2207 return self.pull_request.url(anchor=anchor)
2212 2208
2213 2209 def deletable(self):
2214 2210 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
2215 2211
2216 2212
2217 2213 class ChangesetStatus(Base, BaseModel):
2218 2214 __tablename__ = 'changeset_statuses'
2219 2215 __table_args__ = (
2220 2216 Index('cs_revision_idx', 'revision'),
2221 2217 Index('cs_version_idx', 'version'),
2222 2218 Index('cs_pull_request_id_idx', 'pull_request_id'),
2223 2219 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2224 2220 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2225 2221 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
2226 2222 UniqueConstraint('repo_id', 'revision', 'version'),
2227 2223 _table_args_default_dict,
2228 2224 )
2229 2225
2230 2226 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2231 2227 STATUS_APPROVED = 'approved'
2232 2228 STATUS_REJECTED = 'rejected'
2233 2229 STATUS_UNDER_REVIEW = 'under_review'
2234 2230
2235 2231 STATUSES = [
2236 2232 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
2237 2233 (STATUS_APPROVED, _("Approved")),
2238 2234 (STATUS_REJECTED, _("Rejected")),
2239 2235 (STATUS_UNDER_REVIEW, _("Under review")),
2240 2236 ]
2241 2237
2242 2238 changeset_status_id = Column(Integer(), unique=True, primary_key=True)
2243 2239 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2244 2240 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2245 2241 revision = Column(String(40), nullable=True)
2246 2242 status = Column(String(128), nullable=False, default=DEFAULT)
2247 2243 changeset_comment_id = Column(Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
2248 2244 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
2249 2245 version = Column(Integer(), nullable=False, default=0)
2250 2246 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2251 2247
2252 2248 author = relationship('User')
2253 2249 repo = relationship('Repository')
2254 2250 comment = relationship('ChangesetComment')
2255 2251 pull_request = relationship('PullRequest')
2256 2252
2257 2253 def __unicode__(self):
2258 2254 return u"<%s('%s:%s')>" % (
2259 2255 self.__class__.__name__,
2260 2256 self.status, self.author
2261 2257 )
2262 2258
2263 2259 @classmethod
2264 2260 def get_status_lbl(cls, value):
2265 2261 return dict(cls.STATUSES).get(value)
2266 2262
2267 2263 @property
2268 2264 def status_lbl(self):
2269 2265 return ChangesetStatus.get_status_lbl(self.status)
2270 2266
2271 2267
2272 2268 class PullRequest(Base, BaseModel):
2273 2269 __tablename__ = 'pull_requests'
2274 2270 __table_args__ = (
2275 2271 Index('pr_org_repo_id_idx', 'org_repo_id'),
2276 2272 Index('pr_other_repo_id_idx', 'other_repo_id'),
2277 2273 _table_args_default_dict,
2278 2274 )
2279 2275
2280 2276 # values for .status
2281 2277 STATUS_NEW = u'new'
2282 2278 STATUS_CLOSED = u'closed'
2283 2279
2284 2280 pull_request_id = Column(Integer(), unique=True, primary_key=True)
2285 2281 title = Column(Unicode(255), nullable=False)
2286 2282 description = Column(UnicodeText(10240), nullable=False)
2287 2283 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2288 2284 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2289 2285 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2290 2286 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2291 2287 _revisions = Column('revisions', UnicodeText(20500), nullable=False) # 500 revisions max
2292 2288 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2293 2289 org_ref = Column(Unicode(255), nullable=False)
2294 2290 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2295 2291 other_ref = Column(Unicode(255), nullable=False)
2296 2292
2297 2293 @hybrid_property
2298 2294 def revisions(self):
2299 2295 return self._revisions.split(':')
2300 2296
2301 2297 @revisions.setter
2302 2298 def revisions(self, val):
2303 2299 self._revisions = safe_unicode(':'.join(val))
2304 2300
2305 2301 @property
2306 2302 def org_ref_parts(self):
2307 2303 return self.org_ref.split(':')
2308 2304
2309 2305 @property
2310 2306 def other_ref_parts(self):
2311 2307 return self.other_ref.split(':')
2312 2308
2313 2309 owner = relationship('User')
2314 2310 reviewers = relationship('PullRequestReviewers',
2315 2311 cascade="all, delete-orphan")
2316 2312 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2317 2313 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2318 2314 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2319 2315 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2320 2316 cascade="all, delete-orphan")
2321 2317
2322 2318 def get_reviewer_users(self):
2323 2319 """Like .reviewers, but actually returning the users"""
2324 2320 return User.query() \
2325 2321 .join(PullRequestReviewers) \
2326 2322 .filter(PullRequestReviewers.pull_request == self) \
2327 2323 .order_by(PullRequestReviewers.pull_requests_reviewers_id) \
2328 2324 .all()
2329 2325
2330 2326 def is_closed(self):
2331 2327 return self.status == self.STATUS_CLOSED
2332 2328
2333 2329 def user_review_status(self, user_id):
2334 2330 """Return the user's latest status votes on PR"""
2335 2331 # note: no filtering on repo - that would be redundant
2336 2332 status = ChangesetStatus.query() \
2337 2333 .filter(ChangesetStatus.pull_request == self) \
2338 2334 .filter(ChangesetStatus.user_id == user_id) \
2339 2335 .order_by(ChangesetStatus.version) \
2340 2336 .first()
2341 2337 return str(status.status) if status else ''
2342 2338
2343 2339 @classmethod
2344 2340 def make_nice_id(cls, pull_request_id):
2345 2341 '''Return pull request id nicely formatted for displaying'''
2346 2342 return '#%s' % pull_request_id
2347 2343
2348 2344 def nice_id(self):
2349 2345 '''Return the id of this pull request, nicely formatted for displaying'''
2350 2346 return self.make_nice_id(self.pull_request_id)
2351 2347
2352 2348 def __json__(self):
2353 2349 return dict(
2354 2350 revisions=self.revisions
2355 2351 )
2356 2352
2357 2353 def url(self, **kwargs):
2358 2354 canonical = kwargs.pop('canonical', None)
2359 2355 import kallithea.lib.helpers as h
2360 2356 b = self.org_ref_parts[1]
2361 2357 if b != self.other_ref_parts[1]:
2362 2358 s = '/_/' + b
2363 2359 else:
2364 2360 s = '/_/' + self.title
2365 2361 kwargs['extra'] = urlreadable(s)
2366 2362 if canonical:
2367 2363 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2368 2364 pull_request_id=self.pull_request_id, **kwargs)
2369 2365 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2370 2366 pull_request_id=self.pull_request_id, **kwargs)
2371 2367
2372 2368 class PullRequestReviewers(Base, BaseModel):
2373 2369 __tablename__ = 'pull_request_reviewers'
2374 2370 __table_args__ = (
2375 2371 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2376 2372 _table_args_default_dict,
2377 2373 )
2378 2374
2379 2375 def __init__(self, user=None, pull_request=None):
2380 2376 self.user = user
2381 2377 self.pull_request = pull_request
2382 2378
2383 2379 pull_requests_reviewers_id = Column(Integer(), unique=True, primary_key=True)
2384 2380 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2385 2381 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2386 2382
2387 2383 user = relationship('User')
2388 2384 pull_request = relationship('PullRequest')
2389 2385
2390 2386
2391 2387 class Notification(Base, BaseModel):
2392 2388 __tablename__ = 'notifications'
2393 2389 __table_args__ = (
2394 2390 Index('notification_type_idx', 'type'),
2395 2391 _table_args_default_dict,
2396 2392 )
2397 2393
2398 2394 TYPE_CHANGESET_COMMENT = u'cs_comment'
2399 2395 TYPE_MESSAGE = u'message'
2400 2396 TYPE_MENTION = u'mention'
2401 2397 TYPE_REGISTRATION = u'registration'
2402 2398 TYPE_PULL_REQUEST = u'pull_request'
2403 2399 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
2404 2400
2405 2401 notification_id = Column(Integer(), unique=True, primary_key=True)
2406 2402 subject = Column(Unicode(512), nullable=False)
2407 2403 body = Column(UnicodeText(50000), nullable=False)
2408 2404 created_by = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2409 2405 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2410 2406 type_ = Column('type', Unicode(255), nullable=False)
2411 2407
2412 2408 created_by_user = relationship('User')
2413 2409 notifications_to_users = relationship('UserNotification', cascade="all, delete-orphan")
2414 2410
2415 2411 @property
2416 2412 def recipients(self):
2417 2413 return [x.user for x in UserNotification.query()
2418 2414 .filter(UserNotification.notification == self)
2419 2415 .order_by(UserNotification.user_id.asc()).all()]
2420 2416
2421 2417 @classmethod
2422 2418 def create(cls, created_by, subject, body, recipients, type_=None):
2423 2419 if type_ is None:
2424 2420 type_ = Notification.TYPE_MESSAGE
2425 2421
2426 2422 notification = cls()
2427 2423 notification.created_by_user = created_by
2428 2424 notification.subject = subject
2429 2425 notification.body = body
2430 2426 notification.type_ = type_
2431 2427 notification.created_on = datetime.datetime.now()
2432 2428
2433 2429 for recipient in recipients:
2434 2430 un = UserNotification()
2435 2431 un.notification = notification
2436 2432 un.user_id = recipient.user_id
2437 2433 # Mark notifications to self "pre-read" - should perhaps just be skipped
2438 2434 if recipient == created_by:
2439 2435 un.read = True
2440 2436 Session().add(un)
2441 2437
2442 2438 Session().add(notification)
2443 2439 Session().flush() # assign notificaiton.notification_id
2444 2440 return notification
2445 2441
2446 2442 @property
2447 2443 def description(self):
2448 2444 from kallithea.model.notification import NotificationModel
2449 2445 return NotificationModel().make_description(self)
2450 2446
2451 2447
2452 2448 class UserNotification(Base, BaseModel):
2453 2449 __tablename__ = 'user_to_notification'
2454 2450 __table_args__ = (
2455 2451 UniqueConstraint('user_id', 'notification_id'),
2456 2452 _table_args_default_dict,
2457 2453 )
2458 2454
2459 2455 user_id = Column(Integer(), ForeignKey('users.user_id'), primary_key=True)
2460 2456 notification_id = Column(Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2461 2457 read = Column(Boolean, nullable=False, default=False)
2462 2458 sent_on = Column(DateTime(timezone=False), nullable=True) # FIXME: not nullable?
2463 2459
2464 2460 user = relationship('User')
2465 2461 notification = relationship('Notification')
2466 2462
2467 2463 def mark_as_read(self):
2468 2464 self.read = True
2469 2465 Session().add(self)
2470 2466
2471 2467
2472 2468 class Gist(Base, BaseModel):
2473 2469 __tablename__ = 'gists'
2474 2470 __table_args__ = (
2475 2471 Index('g_gist_access_id_idx', 'gist_access_id'),
2476 2472 Index('g_created_on_idx', 'created_on'),
2477 2473 _table_args_default_dict,
2478 2474 )
2479 2475
2480 2476 GIST_PUBLIC = u'public'
2481 2477 GIST_PRIVATE = u'private'
2482 2478 DEFAULT_FILENAME = u'gistfile1.txt'
2483 2479
2484 2480 gist_id = Column(Integer(), unique=True, primary_key=True)
2485 2481 gist_access_id = Column(Unicode(250), nullable=False)
2486 2482 gist_description = Column(UnicodeText(1024), nullable=False)
2487 2483 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2488 2484 gist_expires = Column(Float(53), nullable=False)
2489 2485 gist_type = Column(Unicode(128), nullable=False)
2490 2486 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2491 2487 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2492 2488
2493 2489 owner = relationship('User')
2494 2490
2495 2491 def __repr__(self):
2496 2492 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2497 2493
2498 2494 @classmethod
2499 2495 def get_or_404(cls, id_):
2500 2496 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2501 2497 if res is None:
2502 2498 raise HTTPNotFound
2503 2499 return res
2504 2500
2505 2501 @classmethod
2506 2502 def get_by_access_id(cls, gist_access_id):
2507 2503 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2508 2504
2509 2505 def gist_url(self):
2510 2506 import kallithea
2511 2507 alias_url = kallithea.CONFIG.get('gist_alias_url')
2512 2508 if alias_url:
2513 2509 return alias_url.replace('{gistid}', self.gist_access_id)
2514 2510
2515 2511 import kallithea.lib.helpers as h
2516 2512 return h.canonical_url('gist', gist_id=self.gist_access_id)
2517 2513
2518 2514 @classmethod
2519 2515 def base_path(cls):
2520 2516 """
2521 2517 Returns base path where all gists are stored
2522 2518
2523 2519 :param cls:
2524 2520 """
2525 2521 from kallithea.model.gist import GIST_STORE_LOC
2526 2522 q = Session().query(Ui) \
2527 2523 .filter(Ui.ui_key == URL_SEP)
2528 2524 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2529 2525 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2530 2526
2531 2527 def get_api_data(self):
2532 2528 """
2533 2529 Common function for generating gist related data for API
2534 2530 """
2535 2531 gist = self
2536 2532 data = dict(
2537 2533 gist_id=gist.gist_id,
2538 2534 type=gist.gist_type,
2539 2535 access_id=gist.gist_access_id,
2540 2536 description=gist.gist_description,
2541 2537 url=gist.gist_url(),
2542 2538 expires=gist.gist_expires,
2543 2539 created_on=gist.created_on,
2544 2540 )
2545 2541 return data
2546 2542
2547 2543 def __json__(self):
2548 2544 data = dict(
2549 2545 )
2550 2546 data.update(self.get_api_data())
2551 2547 return data
2552 2548 ## SCM functions
2553 2549
2554 2550 @property
2555 2551 def scm_instance(self):
2556 2552 from kallithea.lib.vcs import get_repo
2557 2553 base_path = self.base_path()
2558 2554 return get_repo(os.path.join(*map(safe_str,
2559 2555 [base_path, self.gist_access_id])))
2560 2556
2561 2557
2562 2558 class DbMigrateVersion(Base, BaseModel):
2563 2559 __tablename__ = 'db_migrate_version'
2564 2560 __table_args__ = (
2565 2561 _table_args_default_dict,
2566 2562 )
2567 2563
2568 2564 repository_id = Column(String(250), unique=True, primary_key=True)
2569 2565 repository_path = Column(Text, nullable=False)
2570 2566 version = Column(Integer, nullable=False)
@@ -1,599 +1,594 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # Kallithea - config for tests: #
4 4 # initial_repo_scan = true #
5 # vcs_full_cache = false #
6 5 # sqlalchemy and kallithea_test.sqlite #
7 6 # custom logging #
8 7 # #
9 8 # The %(here)s variable will be replaced with the parent directory of this file#
10 9 ################################################################################
11 10 ################################################################################
12 11
13 12 [DEFAULT]
14 13 debug = true
15 14 pdebug = false
16 15
17 16 ################################################################################
18 17 ## Email settings ##
19 18 ## ##
20 19 ## Refer to the documentation ("Email settings") for more details. ##
21 20 ## ##
22 21 ## It is recommended to use a valid sender address that passes access ##
23 22 ## validation and spam filtering in mail servers. ##
24 23 ################################################################################
25 24
26 25 ## 'From' header for application emails. You can optionally add a name.
27 26 ## Default:
28 27 #app_email_from = Kallithea
29 28 ## Examples:
30 29 #app_email_from = Kallithea <kallithea-noreply@example.com>
31 30 #app_email_from = kallithea-noreply@example.com
32 31
33 32 ## Subject prefix for application emails.
34 33 ## A space between this prefix and the real subject is automatically added.
35 34 ## Default:
36 35 #email_prefix =
37 36 ## Example:
38 37 #email_prefix = [Kallithea]
39 38
40 39 ## Recipients for error emails and fallback recipients of application mails.
41 40 ## Multiple addresses can be specified, space-separated.
42 41 ## Only addresses are allowed, do not add any name part.
43 42 ## Default:
44 43 #email_to =
45 44 ## Examples:
46 45 #email_to = admin@example.com
47 46 #email_to = admin@example.com another_admin@example.com
48 47
49 48 ## 'From' header for error emails. You can optionally add a name.
50 49 ## Default:
51 50 #error_email_from = pylons@yourapp.com
52 51 ## Examples:
53 52 #error_email_from = Kallithea Errors <kallithea-noreply@example.com>
54 53 #error_email_from = paste_error@example.com
55 54
56 55 ## SMTP server settings
57 56 ## Only smtp_server is mandatory. All other settings take the specified default
58 57 ## values.
59 58 #smtp_server = smtp.example.com
60 59 #smtp_username =
61 60 #smtp_password =
62 61 #smtp_port = 25
63 62 #smtp_use_tls = false
64 63 #smtp_use_ssl = false
65 64 ## SMTP authentication parameters to use (e.g. LOGIN PLAIN CRAM-MD5, etc.).
66 65 ## If empty, use any of the authentication parameters supported by the server.
67 66 #smtp_auth =
68 67
69 68 [server:main]
70 69 ## PASTE ##
71 70 #use = egg:Paste#http
72 71 ## nr of worker threads to spawn
73 72 #threadpool_workers = 5
74 73 ## max request before thread respawn
75 74 #threadpool_max_requests = 10
76 75 ## option to use threads of process
77 76 #use_threadpool = true
78 77
79 78 ## WAITRESS ##
80 79 use = egg:waitress#main
81 80 ## number of worker threads
82 81 threads = 5
83 82 ## MAX BODY SIZE 100GB
84 83 max_request_body_size = 107374182400
85 84 ## use poll instead of select, fixes fd limits, may not work on old
86 85 ## windows systems.
87 86 #asyncore_use_poll = True
88 87
89 88 ## GUNICORN ##
90 89 #use = egg:gunicorn#main
91 90 ## number of process workers. You must set `instance_id = *` when this option
92 91 ## is set to more than one worker
93 92 #workers = 1
94 93 ## process name
95 94 #proc_name = kallithea
96 95 ## type of worker class, one of sync, eventlet, gevent, tornado
97 96 ## recommended for bigger setup is using of of other than sync one
98 97 #worker_class = sync
99 98 #max_requests = 1000
100 99 ## ammount of time a worker can handle request before it gets killed and
101 100 ## restarted
102 101 #timeout = 3600
103 102
104 103 ## UWSGI ##
105 104 ## run with uwsgi --ini-paste-logged <inifile.ini>
106 105 #[uwsgi]
107 106 #socket = /tmp/uwsgi.sock
108 107 #master = true
109 108 #http = 127.0.0.1:5000
110 109
111 110 ## set as deamon and redirect all output to file
112 111 #daemonize = ./uwsgi_kallithea.log
113 112
114 113 ## master process PID
115 114 #pidfile = ./uwsgi_kallithea.pid
116 115
117 116 ## stats server with workers statistics, use uwsgitop
118 117 ## for monitoring, `uwsgitop 127.0.0.1:1717`
119 118 #stats = 127.0.0.1:1717
120 119 #memory-report = true
121 120
122 121 ## log 5XX errors
123 122 #log-5xx = true
124 123
125 124 ## Set the socket listen queue size.
126 125 #listen = 256
127 126
128 127 ## Gracefully Reload workers after the specified amount of managed requests
129 128 ## (avoid memory leaks).
130 129 #max-requests = 1000
131 130
132 131 ## enable large buffers
133 132 #buffer-size = 65535
134 133
135 134 ## socket and http timeouts ##
136 135 #http-timeout = 3600
137 136 #socket-timeout = 3600
138 137
139 138 ## Log requests slower than the specified number of milliseconds.
140 139 #log-slow = 10
141 140
142 141 ## Exit if no app can be loaded.
143 142 #need-app = true
144 143
145 144 ## Set lazy mode (load apps in workers instead of master).
146 145 #lazy = true
147 146
148 147 ## scaling ##
149 148 ## set cheaper algorithm to use, if not set default will be used
150 149 #cheaper-algo = spare
151 150
152 151 ## minimum number of workers to keep at all times
153 152 #cheaper = 1
154 153
155 154 ## number of workers to spawn at startup
156 155 #cheaper-initial = 1
157 156
158 157 ## maximum number of workers that can be spawned
159 158 #workers = 4
160 159
161 160 ## how many workers should be spawned at a time
162 161 #cheaper-step = 1
163 162
164 163 ## COMMON ##
165 164 host = 127.0.0.1
166 165 #port = 5000
167 166 port = 4999
168 167
169 168 ## middleware for hosting the WSGI application under a URL prefix
170 169 #[filter:proxy-prefix]
171 170 #use = egg:PasteDeploy#prefix
172 171 #prefix = /<your-prefix>
173 172
174 173 [app:main]
175 174 use = egg:kallithea
176 175 ## enable proxy prefix middleware
177 176 #filter-with = proxy-prefix
178 177
179 178 full_stack = true
180 179 static_files = true
181 180 ## Available Languages:
182 181 ## cs de fr hu ja nl_BE pl pt_BR ru sk zh_CN zh_TW
183 182 lang =
184 183 cache_dir = %(here)s/data
185 184 index_dir = %(here)s/data/index
186 185
187 186 ## perform a full repository scan on each server start, this should be
188 187 ## set to false after first startup, to allow faster server restarts.
189 188 #initial_repo_scan = false
190 189 initial_repo_scan = true
191 190
192 191 ## uncomment and set this path to use archive download cache
193 192 archive_cache_dir = %(here)s/tarballcache
194 193
195 194 ## change this to unique ID for security
196 195 app_instance_uuid = test
197 196
198 197 ## cut off limit for large diffs (size in bytes)
199 198 cut_off_limit = 256000
200 199
201 ## use cache version of scm repo everywhere
202 #vcs_full_cache = true
203 vcs_full_cache = false
204
205 200 ## force https in Kallithea, fixes https redirects, assumes it's always https
206 201 force_https = false
207 202
208 203 ## use Strict-Transport-Security headers
209 204 use_htsts = false
210 205
211 206 ## number of commits stats will parse on each iteration
212 207 commit_parse_limit = 25
213 208
214 209 ## path to git executable
215 210 git_path = git
216 211
217 212 ## git rev filter option, --all is the default filter, if you need to
218 213 ## hide all refs in changelog switch this to --branches --tags
219 214 #git_rev_filter = --branches --tags
220 215
221 216 ## RSS feed options
222 217 rss_cut_off_limit = 256000
223 218 rss_items_per_page = 10
224 219 rss_include_diff = false
225 220
226 221 ## options for showing and identifying changesets
227 222 show_sha_length = 12
228 223 #show_revision_number = false
229 224 show_revision_number = true
230 225
231 226 ## gist URL alias, used to create nicer urls for gist. This should be an
232 227 ## url that does rewrites to _admin/gists/<gistid>.
233 228 ## example: http://gist.example.com/{gistid}. Empty means use the internal
234 229 ## Kallithea url, ie. http[s]://kallithea.example.com/_admin/gists/<gistid>
235 230 gist_alias_url =
236 231
237 232 ## white list of API enabled controllers. This allows to add list of
238 233 ## controllers to which access will be enabled by api_key. eg: to enable
239 234 ## api access to raw_files put `FilesController:raw`, to enable access to patches
240 235 ## add `ChangesetController:changeset_patch`. This list should be "," separated
241 236 ## Syntax is <ControllerClass>:<function>. Check debug logs for generated names
242 237 ## Recommended settings below are commented out:
243 238 api_access_controllers_whitelist =
244 239 # ChangesetController:changeset_patch,
245 240 # ChangesetController:changeset_raw,
246 241 # FilesController:raw,
247 242 # FilesController:archivefile
248 243
249 244 ## default encoding used to convert from and to unicode
250 245 ## can be also a comma seperated list of encoding in case of mixed encodings
251 246 default_encoding = utf8
252 247
253 248 ## issue tracker for Kallithea (leave blank to disable, absent for default)
254 249 #bugtracker = https://bitbucket.org/conservancy/kallithea/issues
255 250
256 251 ## issue tracking mapping for commits messages
257 252 ## comment out issue_pat, issue_server, issue_prefix to enable
258 253
259 254 ## pattern to get the issues from commit messages
260 255 ## default one used here is #<numbers> with a regex passive group for `#`
261 256 ## {id} will be all groups matched from this pattern
262 257
263 258 issue_pat = (?:\s*#)(\d+)
264 259
265 260 ## server url to the issue, each {id} will be replaced with match
266 261 ## fetched from the regex and {repo} is replaced with full repository name
267 262 ## including groups {repo_name} is replaced with just name of repo
268 263
269 264 issue_server_link = https://issues.example.com/{repo}/issue/{id}
270 265
271 266 ## prefix to add to link to indicate it's an url
272 267 ## #314 will be replaced by <issue_prefix><id>
273 268
274 269 issue_prefix = #
275 270
276 271 ## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
277 272 ## multiple patterns, to other issues server, wiki or others
278 273 ## below an example how to create a wiki pattern
279 274 # wiki-some-id -> https://wiki.example.com/some-id
280 275
281 276 #issue_pat_wiki = (?:wiki-)(.+)
282 277 #issue_server_link_wiki = https://wiki.example.com/{id}
283 278 #issue_prefix_wiki = WIKI-
284 279
285 280 ## alternative return HTTP header for failed authentication. Default HTTP
286 281 ## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
287 282 ## handling that. Set this variable to 403 to return HTTPForbidden
288 283 auth_ret_code =
289 284
290 285 ## locking return code. When repository is locked return this HTTP code. 2XX
291 286 ## codes don't break the transactions while 4XX codes do
292 287 lock_ret_code = 423
293 288
294 289 ## allows to change the repository location in settings page
295 290 allow_repo_location_change = True
296 291
297 292 ## allows to setup custom hooks in settings page
298 293 allow_custom_hooks_settings = True
299 294
300 295 ## extra extensions for indexing, space separated and without the leading '.'.
301 296 # index.extensions =
302 297 # gemfile
303 298 # lock
304 299
305 300 ## extra filenames for indexing, space separated
306 301 # index.filenames =
307 302 # .dockerignore
308 303 # .editorconfig
309 304 # INSTALL
310 305 # CHANGELOG
311 306
312 307 ####################################
313 308 ### CELERY CONFIG ####
314 309 ####################################
315 310
316 311 use_celery = false
317 312 broker.host = localhost
318 313 broker.vhost = rabbitmqhost
319 314 broker.port = 5672
320 315 broker.user = rabbitmq
321 316 broker.password = qweqwe
322 317
323 318 celery.imports = kallithea.lib.celerylib.tasks
324 319
325 320 celery.result.backend = amqp
326 321 celery.result.dburi = amqp://
327 322 celery.result.serialier = json
328 323
329 324 #celery.send.task.error.emails = true
330 325 #celery.amqp.task.result.expires = 18000
331 326
332 327 celeryd.concurrency = 2
333 328 #celeryd.log.file = celeryd.log
334 329 celeryd.log.level = DEBUG
335 330 celeryd.max.tasks.per.child = 1
336 331
337 332 ## tasks will never be sent to the queue, but executed locally instead.
338 333 celery.always.eager = false
339 334
340 335 ####################################
341 336 ### BEAKER CACHE ####
342 337 ####################################
343 338
344 339 beaker.cache.data_dir = %(here)s/data/cache/data
345 340 beaker.cache.lock_dir = %(here)s/data/cache/lock
346 341
347 342 beaker.cache.regions = short_term,long_term,sql_cache_short
348 343
349 344 beaker.cache.short_term.type = memory
350 345 beaker.cache.short_term.expire = 60
351 346 beaker.cache.short_term.key_length = 256
352 347
353 348 beaker.cache.long_term.type = memory
354 349 beaker.cache.long_term.expire = 36000
355 350 beaker.cache.long_term.key_length = 256
356 351
357 352 beaker.cache.sql_cache_short.type = memory
358 353 #beaker.cache.sql_cache_short.expire = 10
359 354 beaker.cache.sql_cache_short.expire = 1
360 355 beaker.cache.sql_cache_short.key_length = 256
361 356
362 357 ####################################
363 358 ### BEAKER SESSION ####
364 359 ####################################
365 360
366 361 ## Name of session cookie. Should be unique for a given host and path, even when running
367 362 ## on different ports. Otherwise, cookie sessions will be shared and messed up.
368 363 beaker.session.key = kallithea
369 364 ## Sessions should always only be accessible by the browser, not directly by JavaScript.
370 365 beaker.session.httponly = true
371 366 ## Session lifetime. 2592000 seconds is 30 days.
372 367 beaker.session.timeout = 2592000
373 368
374 369 ## Server secret used with HMAC to ensure integrity of cookies.
375 370 beaker.session.secret = {74e0cd75-b339-478b-b129-07dd221def1f}
376 371 ## Further, encrypt the data with AES.
377 372 #beaker.session.encrypt_key = <key_for_encryption>
378 373 #beaker.session.validate_key = <validation_key>
379 374
380 375 ## Type of storage used for the session, current types are
381 376 ## dbm, file, memcached, database, and memory.
382 377
383 378 ## File system storage of session data. (default)
384 379 #beaker.session.type = file
385 380
386 381 ## Cookie only, store all session data inside the cookie. Requires secure secrets.
387 382 #beaker.session.type = cookie
388 383
389 384 ## Database storage of session data.
390 385 #beaker.session.type = ext:database
391 386 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
392 387 #beaker.session.table_name = db_session
393 388
394 389 ############################
395 390 ## ERROR HANDLING SYSTEMS ##
396 391 ############################
397 392
398 393 ####################
399 394 ### [errormator] ###
400 395 ####################
401 396
402 397 ## Errormator is tailored to work with Kallithea, see
403 398 ## http://errormator.com for details how to obtain an account
404 399 ## you must install python package `errormator_client` to make it work
405 400
406 401 ## errormator enabled
407 402 errormator = false
408 403
409 404 errormator.server_url = https://api.errormator.com
410 405 errormator.api_key = YOUR_API_KEY
411 406
412 407 ## TWEAK AMOUNT OF INFO SENT HERE
413 408
414 409 ## enables 404 error logging (default False)
415 410 errormator.report_404 = false
416 411
417 412 ## time in seconds after request is considered being slow (default 1)
418 413 errormator.slow_request_time = 1
419 414
420 415 ## record slow requests in application
421 416 ## (needs to be enabled for slow datastore recording and time tracking)
422 417 errormator.slow_requests = true
423 418
424 419 ## enable hooking to application loggers
425 420 #errormator.logging = true
426 421
427 422 ## minimum log level for log capture
428 423 #errormator.logging.level = WARNING
429 424
430 425 ## send logs only from erroneous/slow requests
431 426 ## (saves API quota for intensive logging)
432 427 errormator.logging_on_error = false
433 428
434 429 ## list of additonal keywords that should be grabbed from environ object
435 430 ## can be string with comma separated list of words in lowercase
436 431 ## (by default client will always send following info:
437 432 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
438 433 ## start with HTTP* this list be extended with additional keywords here
439 434 errormator.environ_keys_whitelist =
440 435
441 436 ## list of keywords that should be blanked from request object
442 437 ## can be string with comma separated list of words in lowercase
443 438 ## (by default client will always blank keys that contain following words
444 439 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
445 440 ## this list be extended with additional keywords set here
446 441 errormator.request_keys_blacklist =
447 442
448 443 ## list of namespaces that should be ignores when gathering log entries
449 444 ## can be string with comma separated list of namespaces
450 445 ## (by default the client ignores own entries: errormator_client.client)
451 446 errormator.log_namespace_blacklist =
452 447
453 448 ################
454 449 ### [sentry] ###
455 450 ################
456 451
457 452 ## sentry is a alternative open source error aggregator
458 453 ## you must install python packages `sentry` and `raven` to enable
459 454
460 455 sentry.dsn = YOUR_DNS
461 456 sentry.servers =
462 457 sentry.name =
463 458 sentry.key =
464 459 sentry.public_key =
465 460 sentry.secret_key =
466 461 sentry.project =
467 462 sentry.site =
468 463 sentry.include_paths =
469 464 sentry.exclude_paths =
470 465
471 466 ################################################################################
472 467 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
473 468 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
474 469 ## execute malicious code after an exception is raised. ##
475 470 ################################################################################
476 471 set debug = false
477 472
478 473 ##################################
479 474 ### LOGVIEW CONFIG ###
480 475 ##################################
481 476
482 477 logview.sqlalchemy = #faa
483 478 logview.pylons.templating = #bfb
484 479 logview.pylons.util = #eee
485 480
486 481 #########################################################
487 482 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
488 483 #########################################################
489 484
490 485 # SQLITE [default]
491 486 #sqlalchemy.db1.url = sqlite:///%(here)s/kallithea.db?timeout=60
492 487 sqlalchemy.db1.url = sqlite:///%(here)s/kallithea_test.sqlite
493 488
494 489 # POSTGRESQL
495 490 #sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
496 491
497 492 # MySQL
498 493 #sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea?charset=utf8
499 494
500 495 # see sqlalchemy docs for others
501 496
502 497 sqlalchemy.db1.echo = false
503 498 sqlalchemy.db1.pool_recycle = 3600
504 499
505 500 ################################
506 501 ### LOGGING CONFIGURATION ####
507 502 ################################
508 503
509 504 [loggers]
510 505 keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
511 506
512 507 [handlers]
513 508 keys = console, console_sql
514 509
515 510 [formatters]
516 511 keys = generic, color_formatter, color_formatter_sql
517 512
518 513 #############
519 514 ## LOGGERS ##
520 515 #############
521 516
522 517 [logger_root]
523 518 #level = NOTSET
524 519 level = DEBUG
525 520 handlers = console
526 521
527 522 [logger_routes]
528 523 level = DEBUG
529 524 handlers =
530 525 qualname = routes.middleware
531 526 ## "level = DEBUG" logs the route matched and routing variables.
532 527 propagate = 1
533 528
534 529 [logger_beaker]
535 530 level = DEBUG
536 531 handlers =
537 532 qualname = beaker.container
538 533 propagate = 1
539 534
540 535 [logger_templates]
541 536 level = INFO
542 537 handlers =
543 538 qualname = pylons.templating
544 539 propagate = 1
545 540
546 541 [logger_kallithea]
547 542 level = DEBUG
548 543 handlers =
549 544 qualname = kallithea
550 545 propagate = 1
551 546
552 547 [logger_sqlalchemy]
553 548 #level = INFO
554 549 level = ERROR
555 550 #handlers = console_sql
556 551 handlers = console
557 552 qualname = sqlalchemy.engine
558 553 propagate = 0
559 554
560 555 [logger_whoosh_indexer]
561 556 level = DEBUG
562 557 handlers =
563 558 qualname = whoosh_indexer
564 559 propagate = 1
565 560
566 561 ##############
567 562 ## HANDLERS ##
568 563 ##############
569 564
570 565 [handler_console]
571 566 class = StreamHandler
572 567 args = (sys.stderr,)
573 568 #level = INFO
574 569 level = NOTSET
575 570 formatter = generic
576 571
577 572 [handler_console_sql]
578 573 class = StreamHandler
579 574 args = (sys.stderr,)
580 575 level = WARN
581 576 formatter = generic
582 577
583 578 ################
584 579 ## FORMATTERS ##
585 580 ################
586 581
587 582 [formatter_generic]
588 583 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
589 584 datefmt = %Y-%m-%d %H:%M:%S
590 585
591 586 [formatter_color_formatter]
592 587 class = kallithea.lib.colored_formatter.ColorFormatter
593 588 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
594 589 datefmt = %Y-%m-%d %H:%M:%S
595 590
596 591 [formatter_color_formatter_sql]
597 592 class = kallithea.lib.colored_formatter.ColorFormatterSql
598 593 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
599 594 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,173 +1,171 b''
1 1 #!/usr/bin/env python2
2 2 """
3 3 Based on kallithea/bin/template.ini.mako, generate
4 4 kallithea/config/deployment.ini_tmpl
5 5 development.ini
6 6 kallithea/tests/test.ini
7 7 """
8 8
9 9 import re
10 10
11 11 makofile = 'kallithea/bin/template.ini.mako'
12 12
13 13 # the mako conditionals used in all other ini files and templates
14 14 selected_mako_conditionals = set([
15 15 "database_engine == 'sqlite'",
16 16 "http_server == 'waitress'",
17 17 "error_aggregation_service == 'errormator'",
18 18 "error_aggregation_service == 'sentry'",
19 19 ])
20 20
21 21 # the mako variables used in all other ini files and templates
22 22 mako_variable_values = {
23 23 'host': '127.0.0.1',
24 24 'port': '5000',
25 25 'here': '%(here)s',
26 26 'uuid()': '${app_instance_uuid}',
27 27 }
28 28
29 29 # files to be generated from the mako template
30 30 ini_files = [
31 31 ('kallithea/config/deployment.ini_tmpl',
32 32 '''
33 33 Kallithea - Example config
34 34
35 35 The %(here)s variable will be replaced with the parent directory of this file
36 36 ''',
37 37 {}, # exactly the same settings as template.ini.mako
38 38 ),
39 39 ('kallithea/tests/test.ini',
40 40 '''
41 41 Kallithea - config for tests:
42 42 initial_repo_scan = true
43 vcs_full_cache = false
44 43 sqlalchemy and kallithea_test.sqlite
45 44 custom logging
46 45
47 46 The %(here)s variable will be replaced with the parent directory of this file
48 47 ''',
49 48 {
50 49 '[server:main]': {
51 50 'port': '4999',
52 51 },
53 52 '[app:main]': {
54 53 'initial_repo_scan': 'true',
55 54 'app_instance_uuid': 'test',
56 'vcs_full_cache': 'false',
57 55 'show_revision_number': 'true',
58 56 'beaker.cache.sql_cache_short.expire': '1',
59 57 'beaker.session.secret': '{74e0cd75-b339-478b-b129-07dd221def1f}',
60 58 'sqlalchemy.db1.url': 'sqlite:///%(here)s/kallithea_test.sqlite',
61 59 },
62 60 '[logger_root]': {
63 61 'level': 'DEBUG',
64 62 },
65 63 '[logger_sqlalchemy]': {
66 64 'level': 'ERROR',
67 65 'handlers': 'console',
68 66 },
69 67 '[handler_console]': {
70 68 'level': 'NOTSET',
71 69 },
72 70 },
73 71 ),
74 72 ('development.ini',
75 73 '''
76 74 Kallithea - Development config:
77 75 listening on *:5000
78 76 sqlite and kallithea.db
79 77 initial_repo_scan = true
80 78 set debug = true
81 79 verbose and colorful logging
82 80
83 81 The %(here)s variable will be replaced with the parent directory of this file
84 82 ''',
85 83 {
86 84 '[server:main]': {
87 85 'host': '0.0.0.0',
88 86 },
89 87 '[app:main]': {
90 88 'initial_repo_scan': 'true',
91 89 'set debug': 'true',
92 90 'app_instance_uuid': 'development-not-secret',
93 91 'beaker.session.secret': 'development-not-secret',
94 92 },
95 93 '[handler_console]': {
96 94 'level': 'DEBUG',
97 95 'formatter': 'color_formatter',
98 96 },
99 97 '[handler_console_sql]': {
100 98 'level': 'DEBUG',
101 99 'formatter': 'color_formatter_sql',
102 100 },
103 101 },
104 102 ),
105 103 ]
106 104
107 105
108 106 def main():
109 107 # make sure all mako lines starting with '#' (the '##' comments) are marked up as <text>
110 108 print 'reading:', makofile
111 109 mako_org = file(makofile).read()
112 110 mako_no_text_markup = re.sub(r'</?%text>', '', mako_org)
113 111 mako_marked_up = re.sub(r'\n(##.*)', r'\n<%text>\1</%text>', mako_no_text_markup, flags=re.MULTILINE)
114 112 if mako_marked_up != mako_org:
115 113 print 'writing:', makofile
116 114 file(makofile, 'w').write(mako_marked_up)
117 115
118 116 # select the right mako conditionals for the other less sophisticated formats
119 117 def sub_conditionals(m):
120 118 """given a %if...%endif match, replace with just the selected
121 119 conditional sections enabled and the rest as comments
122 120 """
123 121 conditional_lines = m.group(1)
124 122 def sub_conditional(m):
125 123 """given a conditional and the corresponding lines, return them raw
126 124 or commented out, based on whether conditional is selected
127 125 """
128 126 criteria, lines = m.groups()
129 127 if criteria not in selected_mako_conditionals:
130 128 lines = '\n'.join((l if not l or l.startswith('#') else '#' + l) for l in lines.split('\n'))
131 129 return lines
132 130 conditional_lines = re.sub(r'^%(?:el)?if (.*):\n((?:^[^%\n].*\n|\n)*)',
133 131 sub_conditional, conditional_lines, flags=re.MULTILINE)
134 132 return conditional_lines
135 133 mako_no_conditionals = re.sub(r'^(%if .*\n(?:[^%\n].*\n|%elif .*\n|\n)*)%endif\n',
136 134 sub_conditionals, mako_no_text_markup, flags=re.MULTILINE)
137 135
138 136 # expand mako variables
139 137 def pyrepl(m):
140 138 return mako_variable_values.get(m.group(1), m.group(0))
141 139 mako_no_variables = re.sub(r'\${([^}]*)}', pyrepl, mako_no_conditionals)
142 140
143 141 # remove utf-8 coding header
144 142 base_ini = re.sub(r'^## -\*- coding: utf-8 -\*-\n', '', mako_no_variables)
145 143
146 144 # create ini files
147 145 for fn, desc, settings in ini_files:
148 146 print 'updating:', fn
149 147 ini_lines = re.sub(
150 148 '# Kallithea - config file generated with kallithea-config *#\n',
151 149 ''.join('# %-77s#\n' % l.strip() for l in desc.strip().split('\n')),
152 150 base_ini)
153 151 def process_section(m):
154 152 """process a ini section, replacing values as necessary"""
155 153 sectionname, lines = m.groups()
156 154 if sectionname in settings:
157 155 section_settings = settings[sectionname]
158 156 def process_line(m):
159 157 """process a section line and update value if necessary"""
160 158 setting, value = m.groups()
161 159 line = m.group(0)
162 160 if setting in section_settings:
163 161 line = '%s = %s' % (setting, section_settings[setting])
164 162 if '$' not in value:
165 163 line = '#%s = %s\n%s' % (setting, value, line)
166 164 return line.rstrip()
167 165 lines = re.sub(r'^([^#\n].*) = ?(.*)', process_line, lines, flags=re.MULTILINE)
168 166 return sectionname + '\n' + lines
169 167 ini_lines = re.sub(r'^(\[.*\])\n((?:(?:[^[\n].*)?\n)*)', process_section, ini_lines, flags=re.MULTILINE)
170 168 file(fn, 'w').write(ini_lines)
171 169
172 170 if __name__ == '__main__':
173 171 main()
General Comments 0
You need to be logged in to leave comments. Login now