##// END OF EJS Templates
emails: added reply link to comment type emails...
dan -
r4050:b3fe0fcc default
parent child Browse files
Show More
@@ -1,715 +1,719 b''
1 1
2 2
3 3 ################################################################################
4 4 ## RHODECODE COMMUNITY EDITION CONFIGURATION ##
5 5 ################################################################################
6 6
7 7 [DEFAULT]
8 8 ## Debug flag sets all loggers to debug, and enables request tracking
9 9 debug = false
10 10
11 11 ################################################################################
12 12 ## EMAIL CONFIGURATION ##
13 13 ## Uncomment and replace with the email address which should receive ##
14 14 ## any error reports after an application crash ##
15 15 ## Additionally these settings will be used by the RhodeCode mailing system ##
16 16 ################################################################################
17 17
18 18 ## prefix all emails subjects with given prefix, helps filtering out emails
19 19 #email_prefix = [RhodeCode]
20 20
21 21 ## email FROM address all mails will be sent
22 22 #app_email_from = rhodecode-noreply@localhost
23 23
24 24 #smtp_server = mail.server.com
25 25 #smtp_username =
26 26 #smtp_password =
27 27 #smtp_port =
28 28 #smtp_use_tls = false
29 29 #smtp_use_ssl = true
30 30
31 31 [server:main]
32 32 ## COMMON ##
33 33 host = 127.0.0.1
34 34 port = 5000
35 35
36 36 ###########################################################
37 37 ## WAITRESS WSGI SERVER - Recommended for Development ####
38 38 ###########################################################
39 39
40 40 #use = egg:waitress#main
41 41 ## number of worker threads
42 42 #threads = 5
43 43 ## MAX BODY SIZE 100GB
44 44 #max_request_body_size = 107374182400
45 45 ## Use poll instead of select, fixes file descriptors limits problems.
46 46 ## May not work on old windows systems.
47 47 #asyncore_use_poll = true
48 48
49 49
50 50 ##########################
51 51 ## GUNICORN WSGI SERVER ##
52 52 ##########################
53 53 ## run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
54 54
55 55 use = egg:gunicorn#main
56 56 ## Sets the number of process workers. More workers means more concurrent connections
57 57 ## RhodeCode can handle at the same time. Each additional worker also it increases
58 58 ## memory usage as each has it's own set of caches.
59 59 ## Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
60 60 ## than 8-10 unless for really big deployments .e.g 700-1000 users.
61 61 ## `instance_id = *` must be set in the [app:main] section below (which is the default)
62 62 ## when using more than 1 worker.
63 63 workers = 2
64 64 ## process name visible in process list
65 65 proc_name = rhodecode
66 66 ## type of worker class, one of sync, gevent
67 67 ## recommended for bigger setup is using of of other than sync one
68 68 worker_class = gevent
69 69 ## The maximum number of simultaneous clients. Valid only for Gevent
70 70 worker_connections = 10
71 71 ## max number of requests that worker will handle before being gracefully
72 72 ## restarted, could prevent memory leaks
73 73 max_requests = 1000
74 74 max_requests_jitter = 30
75 75 ## amount of time a worker can spend with handling a request before it
76 76 ## gets killed and restarted. Set to 6hrs
77 77 timeout = 21600
78 78
79 79
80 80 ## prefix middleware for RhodeCode.
81 81 ## recommended when using proxy setup.
82 82 ## allows to set RhodeCode under a prefix in server.
83 83 ## eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
84 84 ## And set your prefix like: `prefix = /custom_prefix`
85 85 ## be sure to also set beaker.session.cookie_path = /custom_prefix if you need
86 86 ## to make your cookies only work on prefix url
87 87 [filter:proxy-prefix]
88 88 use = egg:PasteDeploy#prefix
89 89 prefix = /
90 90
91 91 [app:main]
92 92 ## The %(here)s variable will be replaced with the absolute path of parent directory
93 93 ## of this file
94 94 ## In addition ENVIRONMENT variables usage is possible, e.g
95 95 ## sqlalchemy.db1.url = {ENV_RC_DB_URL}
96 96
97 97 use = egg:rhodecode-enterprise-ce
98 98
99 99 ## enable proxy prefix middleware, defined above
100 100 #filter-with = proxy-prefix
101 101
102 102 ## encryption key used to encrypt social plugin tokens,
103 103 ## remote_urls with credentials etc, if not set it defaults to
104 104 ## `beaker.session.secret`
105 105 #rhodecode.encrypted_values.secret =
106 106
107 107 ## decryption strict mode (enabled by default). It controls if decryption raises
108 108 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
109 109 #rhodecode.encrypted_values.strict = false
110 110
111 111 ## Pick algorithm for encryption. Either fernet (more secure) or aes (default)
112 112 ## fernet is safer, and we strongly recommend switching to it.
113 113 ## Due to backward compatibility aes is used as default.
114 114 #rhodecode.encrypted_values.algorithm = fernet
115 115
116 116 ## return gzipped responses from RhodeCode (static files/application)
117 117 gzip_responses = false
118 118
119 119 ## auto-generate javascript routes file on startup
120 120 generate_js_files = false
121 121
122 122 ## System global default language.
123 123 ## All available languages: en(default), be, de, es, fr, it, ja, pl, pt, ru, zh
124 124 lang = en
125 125
126 126 ## Perform a full repository scan and import on each server start.
127 127 ## Settings this to true could lead to very long startup time.
128 128 startup.import_repos = false
129 129
130 130 ## Uncomment and set this path to use archive download cache.
131 131 ## Once enabled, generated archives will be cached at this location
132 132 ## and served from the cache during subsequent requests for the same archive of
133 133 ## the repository.
134 134 #archive_cache_dir = /tmp/tarballcache
135 135
136 136 ## URL at which the application is running. This is used for Bootstrapping
137 137 ## requests in context when no web request is available. Used in ishell, or
138 138 ## SSH calls. Set this for events to receive proper url for SSH calls.
139 139 app.base_url = http://rhodecode.local
140 140
141 141 ## Unique application ID. Should be a random unique string for security.
142 142 app_instance_uuid = rc-production
143 143
144 144 ## Cut off limit for large diffs (size in bytes). If overall diff size on
145 145 ## commit, or pull request exceeds this limit this diff will be displayed
146 146 ## partially. E.g 512000 == 512Kb
147 147 cut_off_limit_diff = 512000
148 148
149 149 ## Cut off limit for large files inside diffs (size in bytes). Each individual
150 150 ## file inside diff which exceeds this limit will be displayed partially.
151 151 ## E.g 128000 == 128Kb
152 152 cut_off_limit_file = 128000
153 153
154 154 ## use cached version of vcs repositories everywhere. Recommended to be `true`
155 155 vcs_full_cache = true
156 156
157 157 ## Force https in RhodeCode, fixes https redirects, assumes it's always https.
158 158 ## Normally this is controlled by proper http flags sent from http server
159 159 force_https = false
160 160
161 161 ## use Strict-Transport-Security headers
162 162 use_htsts = false
163 163
164 164 # Set to true if your repos are exposed using the dumb protocol
165 165 git_update_server_info = false
166 166
167 167 ## RSS/ATOM feed options
168 168 rss_cut_off_limit = 256000
169 169 rss_items_per_page = 10
170 170 rss_include_diff = false
171 171
172 172 ## gist URL alias, used to create nicer urls for gist. This should be an
173 173 ## url that does rewrites to _admin/gists/{gistid}.
174 174 ## example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
175 175 ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
176 176 gist_alias_url =
177 177
178 178 ## List of views (using glob pattern syntax) that AUTH TOKENS could be
179 179 ## used for access.
180 180 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
181 181 ## came from the the logged in user who own this authentication token.
182 182 ## Additionally @TOKEN syntax can be used to bound the view to specific
183 183 ## authentication token. Such view would be only accessible when used together
184 184 ## with this authentication token
185 185 ##
186 186 ## list of all views can be found under `/_admin/permissions/auth_token_access`
187 187 ## The list should be "," separated and on a single line.
188 188 ##
189 189 ## Most common views to enable:
190 190 # RepoCommitsView:repo_commit_download
191 191 # RepoCommitsView:repo_commit_patch
192 192 # RepoCommitsView:repo_commit_raw
193 193 # RepoCommitsView:repo_commit_raw@TOKEN
194 194 # RepoFilesView:repo_files_diff
195 195 # RepoFilesView:repo_archivefile
196 196 # RepoFilesView:repo_file_raw
197 197 # GistView:*
198 198 api_access_controllers_whitelist =
199 199
200 200 ## Default encoding used to convert from and to unicode
201 201 ## can be also a comma separated list of encoding in case of mixed encodings
202 202 default_encoding = UTF-8
203 203
204 204 ## instance-id prefix
205 205 ## a prefix key for this instance used for cache invalidation when running
206 206 ## multiple instances of RhodeCode, make sure it's globally unique for
207 207 ## all running RhodeCode instances. Leave empty if you don't use it
208 208 instance_id =
209 209
210 210 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
211 211 ## of an authentication plugin also if it is disabled by it's settings.
212 212 ## This could be useful if you are unable to log in to the system due to broken
213 213 ## authentication settings. Then you can enable e.g. the internal RhodeCode auth
214 214 ## module to log in again and fix the settings.
215 215 ##
216 216 ## Available builtin plugin IDs (hash is part of the ID):
217 217 ## egg:rhodecode-enterprise-ce#rhodecode
218 218 ## egg:rhodecode-enterprise-ce#pam
219 219 ## egg:rhodecode-enterprise-ce#ldap
220 220 ## egg:rhodecode-enterprise-ce#jasig_cas
221 221 ## egg:rhodecode-enterprise-ce#headers
222 222 ## egg:rhodecode-enterprise-ce#crowd
223 223 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
224 224
225 225 ## alternative return HTTP header for failed authentication. Default HTTP
226 226 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
227 227 ## handling that causing a series of failed authentication calls.
228 228 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
229 229 ## This will be served instead of default 401 on bad authentication
230 230 auth_ret_code =
231 231
232 232 ## use special detection method when serving auth_ret_code, instead of serving
233 233 ## ret_code directly, use 401 initially (Which triggers credentials prompt)
234 234 ## and then serve auth_ret_code to clients
235 235 auth_ret_code_detection = false
236 236
237 237 ## locking return code. When repository is locked return this HTTP code. 2XX
238 238 ## codes don't break the transactions while 4XX codes do
239 239 lock_ret_code = 423
240 240
241 241 ## allows to change the repository location in settings page
242 242 allow_repo_location_change = true
243 243
244 244 ## allows to setup custom hooks in settings page
245 245 allow_custom_hooks_settings = true
246 246
247 247 ## Generated license token required for EE edition license.
248 248 ## New generated token value can be found in Admin > settings > license page.
249 249 license_token =
250 250
251 251 ## This flag would hide sensitive information on the license page
252 252 license.hide_license_info = false
253 253
254 254 ## supervisor connection uri, for managing supervisor and logs.
255 255 supervisor.uri =
256 256 ## supervisord group name/id we only want this RC instance to handle
257 257 supervisor.group_id = prod
258 258
259 259 ## Display extended labs settings
260 260 labs_settings_active = true
261 261
262 262 ## Custom exception store path, defaults to TMPDIR
263 263 ## This is used to store exception from RhodeCode in shared directory
264 264 #exception_tracker.store_path =
265 265
266 266 ## File store configuration. This is used to store and serve uploaded files
267 267 file_store.enabled = true
268 268 ## Storage backend, available options are: local
269 269 file_store.backend = local
270 270 ## path to store the uploaded binaries
271 271 file_store.storage_path = %(here)s/data/file_store
272 272
273 273
274 274 ####################################
275 275 ### CELERY CONFIG ####
276 276 ####################################
277 277 ## run: /path/to/celery worker \
278 278 ## -E --beat --app rhodecode.lib.celerylib.loader \
279 279 ## --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
280 280 ## --loglevel DEBUG --ini /path/to/rhodecode.ini
281 281
282 282 use_celery = false
283 283
284 284 ## connection url to the message broker (default redis)
285 285 celery.broker_url = redis://localhost:6379/8
286 286
287 287 ## rabbitmq example
288 288 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
289 289
290 290 ## maximum tasks to execute before worker restart
291 291 celery.max_tasks_per_child = 100
292 292
293 293 ## tasks will never be sent to the queue, but executed locally instead.
294 294 celery.task_always_eager = false
295 295
296 296 #####################################
297 297 ### DOGPILE CACHE ####
298 298 #####################################
299 299 ## Default cache dir for caches. Putting this into a ramdisk
300 300 ## can boost performance, eg. /tmpfs/data_ramdisk, however this directory might require
301 301 ## large amount of space
302 302 cache_dir = %(here)s/data
303 303
304 304 ## `cache_perms` cache settings for permission tree, auth TTL.
305 305 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
306 306 rc_cache.cache_perms.expiration_time = 300
307 307
308 308 ## alternative `cache_perms` redis backend with distributed lock
309 309 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
310 310 #rc_cache.cache_perms.expiration_time = 300
311 311 ## redis_expiration_time needs to be greater then expiration_time
312 312 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
313 313 #rc_cache.cache_perms.arguments.socket_timeout = 30
314 314 #rc_cache.cache_perms.arguments.host = localhost
315 315 #rc_cache.cache_perms.arguments.port = 6379
316 316 #rc_cache.cache_perms.arguments.db = 0
317 317 ## more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
318 318 #rc_cache.cache_perms.arguments.distributed_lock = true
319 319
320 320 ## `cache_repo` cache settings for FileTree, Readme, RSS FEEDS
321 321 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
322 322 rc_cache.cache_repo.expiration_time = 2592000
323 323
324 324 ## alternative `cache_repo` redis backend with distributed lock
325 325 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
326 326 #rc_cache.cache_repo.expiration_time = 2592000
327 327 ## redis_expiration_time needs to be greater then expiration_time
328 328 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
329 329 #rc_cache.cache_repo.arguments.socket_timeout = 30
330 330 #rc_cache.cache_repo.arguments.host = localhost
331 331 #rc_cache.cache_repo.arguments.port = 6379
332 332 #rc_cache.cache_repo.arguments.db = 1
333 333 ## more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
334 334 #rc_cache.cache_repo.arguments.distributed_lock = true
335 335
336 336 ## cache settings for SQL queries, this needs to use memory type backend
337 337 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
338 338 rc_cache.sql_cache_short.expiration_time = 30
339 339
340 340 ## `cache_repo_longterm` cache for repo object instances, this needs to use memory
341 341 ## type backend as the objects kept are not pickle serializable
342 342 rc_cache.cache_repo_longterm.backend = dogpile.cache.rc.memory_lru
343 343 ## by default we use 96H, this is using invalidation on push anyway
344 344 rc_cache.cache_repo_longterm.expiration_time = 345600
345 345 ## max items in LRU cache, reduce this number to save memory, and expire last used
346 346 ## cached objects
347 347 rc_cache.cache_repo_longterm.max_size = 10000
348 348
349 349
350 350 ####################################
351 351 ### BEAKER SESSION ####
352 352 ####################################
353 353
354 354 ## .session.type is type of storage options for the session, current allowed
355 355 ## types are file, ext:memcached, ext:redis, ext:database, and memory (default).
356 356 beaker.session.type = file
357 357 beaker.session.data_dir = %(here)s/data/sessions
358 358
359 359 ## redis sessions
360 360 #beaker.session.type = ext:redis
361 361 #beaker.session.url = redis://127.0.0.1:6379/2
362 362
363 363 ## db based session, fast, and allows easy management over logged in users
364 364 #beaker.session.type = ext:database
365 365 #beaker.session.table_name = db_session
366 366 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
367 367 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
368 368 #beaker.session.sa.pool_recycle = 3600
369 369 #beaker.session.sa.echo = false
370 370
371 371 beaker.session.key = rhodecode
372 372 beaker.session.secret = production-rc-uytcxaz
373 373 beaker.session.lock_dir = %(here)s/data/sessions/lock
374 374
375 375 ## Secure encrypted cookie. Requires AES and AES python libraries
376 376 ## you must disable beaker.session.secret to use this
377 377 #beaker.session.encrypt_key = key_for_encryption
378 378 #beaker.session.validate_key = validation_key
379 379
380 380 ## sets session as invalid(also logging out user) if it haven not been
381 381 ## accessed for given amount of time in seconds
382 382 beaker.session.timeout = 2592000
383 383 beaker.session.httponly = true
384 384 ## Path to use for the cookie. Set to prefix if you use prefix middleware
385 385 #beaker.session.cookie_path = /custom_prefix
386 386
387 387 ## uncomment for https secure cookie
388 388 beaker.session.secure = false
389 389
390 390 ## auto save the session to not to use .save()
391 391 beaker.session.auto = false
392 392
393 393 ## default cookie expiration time in seconds, set to `true` to set expire
394 394 ## at browser close
395 395 #beaker.session.cookie_expires = 3600
396 396
397 397 ###################################
398 398 ## SEARCH INDEXING CONFIGURATION ##
399 399 ###################################
400 400 ## Full text search indexer is available in rhodecode-tools under
401 401 ## `rhodecode-tools index` command
402 402
403 403 ## WHOOSH Backend, doesn't require additional services to run
404 404 ## it works good with few dozen repos
405 405 search.module = rhodecode.lib.index.whoosh
406 406 search.location = %(here)s/data/index
407 407
408 408 ########################################
409 409 ### CHANNELSTREAM CONFIG ####
410 410 ########################################
411 411 ## channelstream enables persistent connections and live notification
412 412 ## in the system. It's also used by the chat system
413 413
414 414 channelstream.enabled = false
415 415
416 416 ## server address for channelstream server on the backend
417 417 channelstream.server = 127.0.0.1:9800
418 418
419 419 ## location of the channelstream server from outside world
420 420 ## use ws:// for http or wss:// for https. This address needs to be handled
421 421 ## by external HTTP server such as Nginx or Apache
422 422 ## see Nginx/Apache configuration examples in our docs
423 423 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
424 424 channelstream.secret = secret
425 425 channelstream.history.location = %(here)s/channelstream_history
426 426
427 427 ## Internal application path that Javascript uses to connect into.
428 428 ## If you use proxy-prefix the prefix should be added before /_channelstream
429 429 channelstream.proxy_path = /_channelstream
430 430
431 ## Live chat for commits/pull requests. Requires CHANNELSTREAM to be enabled
432 ## and configured. (EE edition only)
433 chat.enabled = true
434
431 435
432 436 ###################################
433 437 ## APPENLIGHT CONFIG ##
434 438 ###################################
435 439
436 440 ## Appenlight is tailored to work with RhodeCode, see
437 441 ## http://appenlight.com for details how to obtain an account
438 442
439 443 ## Appenlight integration enabled
440 444 appenlight = false
441 445
442 446 appenlight.server_url = https://api.appenlight.com
443 447 appenlight.api_key = YOUR_API_KEY
444 448 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
445 449
446 450 ## used for JS client
447 451 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
448 452
449 453 ## TWEAK AMOUNT OF INFO SENT HERE
450 454
451 455 ## enables 404 error logging (default False)
452 456 appenlight.report_404 = false
453 457
454 458 ## time in seconds after request is considered being slow (default 1)
455 459 appenlight.slow_request_time = 1
456 460
457 461 ## record slow requests in application
458 462 ## (needs to be enabled for slow datastore recording and time tracking)
459 463 appenlight.slow_requests = true
460 464
461 465 ## enable hooking to application loggers
462 466 appenlight.logging = true
463 467
464 468 ## minimum log level for log capture
465 469 appenlight.logging.level = WARNING
466 470
467 471 ## send logs only from erroneous/slow requests
468 472 ## (saves API quota for intensive logging)
469 473 appenlight.logging_on_error = false
470 474
471 475 ## list of additional keywords that should be grabbed from environ object
472 476 ## can be string with comma separated list of words in lowercase
473 477 ## (by default client will always send following info:
474 478 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
475 479 ## start with HTTP* this list be extended with additional keywords here
476 480 appenlight.environ_keys_whitelist =
477 481
478 482 ## list of keywords that should be blanked from request object
479 483 ## can be string with comma separated list of words in lowercase
480 484 ## (by default client will always blank keys that contain following words
481 485 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
482 486 ## this list be extended with additional keywords set here
483 487 appenlight.request_keys_blacklist =
484 488
485 489 ## list of namespaces that should be ignores when gathering log entries
486 490 ## can be string with comma separated list of namespaces
487 491 ## (by default the client ignores own entries: appenlight_client.client)
488 492 appenlight.log_namespace_blacklist =
489 493
490 494
491 495 ###########################################
492 496 ### MAIN RHODECODE DATABASE CONFIG ###
493 497 ###########################################
494 498 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
495 499 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
496 500 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode?charset=utf8
497 501 # pymysql is an alternative driver for MySQL, use in case of problems with default one
498 502 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
499 503
500 504 sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
501 505
502 506 # see sqlalchemy docs for other advanced settings
503 507
504 508 ## print the sql statements to output
505 509 sqlalchemy.db1.echo = false
506 510 ## recycle the connections after this amount of seconds
507 511 sqlalchemy.db1.pool_recycle = 3600
508 512
509 513 ## the number of connections to keep open inside the connection pool.
510 514 ## 0 indicates no limit
511 515 #sqlalchemy.db1.pool_size = 5
512 516
513 517 ## the number of connections to allow in connection pool "overflow", that is
514 518 ## connections that can be opened above and beyond the pool_size setting,
515 519 ## which defaults to five.
516 520 #sqlalchemy.db1.max_overflow = 10
517 521
518 522 ## Connection check ping, used to detect broken database connections
519 523 ## could be enabled to better handle cases if MySQL has gone away errors
520 524 #sqlalchemy.db1.ping_connection = true
521 525
522 526 ##################
523 527 ### VCS CONFIG ###
524 528 ##################
525 529 vcs.server.enable = true
526 530 vcs.server = localhost:9900
527 531
528 532 ## Web server connectivity protocol, responsible for web based VCS operations
529 533 ## Available protocols are:
530 534 ## `http` - use http-rpc backend (default)
531 535 vcs.server.protocol = http
532 536
533 537 ## Push/Pull operations protocol, available options are:
534 538 ## `http` - use http-rpc backend (default)
535 539 vcs.scm_app_implementation = http
536 540
537 541 ## Push/Pull operations hooks protocol, available options are:
538 542 ## `http` - use http-rpc backend (default)
539 543 vcs.hooks.protocol = http
540 544
541 545 ## Host on which this instance is listening for hooks. If vcsserver is in other location
542 546 ## this should be adjusted.
543 547 vcs.hooks.host = 127.0.0.1
544 548
545 549 vcs.server.log_level = info
546 550 ## Start VCSServer with this instance as a subprocess, useful for development
547 551 vcs.start_server = false
548 552
549 553 ## List of enabled VCS backends, available options are:
550 554 ## `hg` - mercurial
551 555 ## `git` - git
552 556 ## `svn` - subversion
553 557 vcs.backends = hg, git, svn
554 558
555 559 vcs.connection_timeout = 3600
556 560 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
557 561 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
558 562 #vcs.svn.compatible_version = pre-1.8-compatible
559 563
560 564
561 565 ############################################################
562 566 ### Subversion proxy support (mod_dav_svn) ###
563 567 ### Maps RhodeCode repo groups into SVN paths for Apache ###
564 568 ############################################################
565 569 ## Enable or disable the config file generation.
566 570 svn.proxy.generate_config = false
567 571 ## Generate config file with `SVNListParentPath` set to `On`.
568 572 svn.proxy.list_parent_path = true
569 573 ## Set location and file name of generated config file.
570 574 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
571 575 ## alternative mod_dav config template. This needs to be a mako template
572 576 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
573 577 ## Used as a prefix to the `Location` block in the generated config file.
574 578 ## In most cases it should be set to `/`.
575 579 svn.proxy.location_root = /
576 580 ## Command to reload the mod dav svn configuration on change.
577 581 ## Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
578 582 ## Make sure user who runs RhodeCode process is allowed to reload Apache
579 583 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
580 584 ## If the timeout expires before the reload command finishes, the command will
581 585 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
582 586 #svn.proxy.reload_timeout = 10
583 587
584 588 ############################################################
585 589 ### SSH Support Settings ###
586 590 ############################################################
587 591
588 592 ## Defines if a custom authorized_keys file should be created and written on
589 593 ## any change user ssh keys. Setting this to false also disables possibility
590 594 ## of adding SSH keys by users from web interface. Super admins can still
591 595 ## manage SSH Keys.
592 596 ssh.generate_authorized_keyfile = false
593 597
594 598 ## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
595 599 # ssh.authorized_keys_ssh_opts =
596 600
597 601 ## Path to the authorized_keys file where the generate entries are placed.
598 602 ## It is possible to have multiple key files specified in `sshd_config` e.g.
599 603 ## AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
600 604 ssh.authorized_keys_file_path = ~/.ssh/authorized_keys_rhodecode
601 605
602 606 ## Command to execute the SSH wrapper. The binary is available in the
603 607 ## RhodeCode installation directory.
604 608 ## e.g ~/.rccontrol/community-1/profile/bin/rc-ssh-wrapper
605 609 ssh.wrapper_cmd = ~/.rccontrol/community-1/rc-ssh-wrapper
606 610
607 611 ## Allow shell when executing the ssh-wrapper command
608 612 ssh.wrapper_cmd_allow_shell = false
609 613
610 614 ## Enables logging, and detailed output send back to the client during SSH
611 615 ## operations. Useful for debugging, shouldn't be used in production.
612 616 ssh.enable_debug_logging = false
613 617
614 618 ## Paths to binary executable, by default they are the names, but we can
615 619 ## override them if we want to use a custom one
616 620 ssh.executable.hg = ~/.rccontrol/vcsserver-1/profile/bin/hg
617 621 ssh.executable.git = ~/.rccontrol/vcsserver-1/profile/bin/git
618 622 ssh.executable.svn = ~/.rccontrol/vcsserver-1/profile/bin/svnserve
619 623
620 624 ## Enables SSH key generator web interface. Disabling this still allows users
621 625 ## to add their own keys.
622 626 ssh.enable_ui_key_generator = true
623 627
624 628
625 629 ## Dummy marker to add new entries after.
626 630 ## Add any custom entries below. Please don't remove.
627 631 custom.conf = 1
628 632
629 633
630 634 ################################
631 635 ### LOGGING CONFIGURATION ####
632 636 ################################
633 637 [loggers]
634 638 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
635 639
636 640 [handlers]
637 641 keys = console, console_sql
638 642
639 643 [formatters]
640 644 keys = generic, color_formatter, color_formatter_sql
641 645
642 646 #############
643 647 ## LOGGERS ##
644 648 #############
645 649 [logger_root]
646 650 level = NOTSET
647 651 handlers = console
648 652
649 653 [logger_sqlalchemy]
650 654 level = INFO
651 655 handlers = console_sql
652 656 qualname = sqlalchemy.engine
653 657 propagate = 0
654 658
655 659 [logger_beaker]
656 660 level = DEBUG
657 661 handlers =
658 662 qualname = beaker.container
659 663 propagate = 1
660 664
661 665 [logger_rhodecode]
662 666 level = DEBUG
663 667 handlers =
664 668 qualname = rhodecode
665 669 propagate = 1
666 670
667 671 [logger_ssh_wrapper]
668 672 level = DEBUG
669 673 handlers =
670 674 qualname = ssh_wrapper
671 675 propagate = 1
672 676
673 677 [logger_celery]
674 678 level = DEBUG
675 679 handlers =
676 680 qualname = celery
677 681
678 682
679 683 ##############
680 684 ## HANDLERS ##
681 685 ##############
682 686
683 687 [handler_console]
684 688 class = StreamHandler
685 689 args = (sys.stderr, )
686 690 level = INFO
687 691 formatter = generic
688 692
689 693 [handler_console_sql]
690 694 # "level = DEBUG" logs SQL queries and results.
691 695 # "level = INFO" logs SQL queries.
692 696 # "level = WARN" logs neither. (Recommended for production systems.)
693 697 class = StreamHandler
694 698 args = (sys.stderr, )
695 699 level = WARN
696 700 formatter = generic
697 701
698 702 ################
699 703 ## FORMATTERS ##
700 704 ################
701 705
702 706 [formatter_generic]
703 707 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
704 708 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
705 709 datefmt = %Y-%m-%d %H:%M:%S
706 710
707 711 [formatter_color_formatter]
708 712 class = rhodecode.lib.logging_formatter.ColorFormatter
709 713 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
710 714 datefmt = %Y-%m-%d %H:%M:%S
711 715
712 716 [formatter_color_formatter_sql]
713 717 class = rhodecode.lib.logging_formatter.ColorFormatterSql
714 718 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
715 719 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,338 +1,347 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23 import datetime
24 24
25 25 from pyramid.view import view_config
26 26 from pyramid.renderers import render_to_response
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib.celerylib import run_task, tasks
29 29 from rhodecode.lib.utils2 import AttributeDict
30 30 from rhodecode.model.db import User
31 31 from rhodecode.model.notification import EmailNotificationModel
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class DebugStyleView(BaseAppView):
37 37 def load_default_context(self):
38 38 c = self._get_local_tmpl_context()
39 39
40 40 return c
41 41
42 42 @view_config(
43 43 route_name='debug_style_home', request_method='GET',
44 44 renderer=None)
45 45 def index(self):
46 46 c = self.load_default_context()
47 47 c.active = 'index'
48 48
49 49 return render_to_response(
50 50 'debug_style/index.html', self._get_template_context(c),
51 51 request=self.request)
52 52
53 53 @view_config(
54 54 route_name='debug_style_email', request_method='GET',
55 55 renderer=None)
56 56 @view_config(
57 57 route_name='debug_style_email_plain_rendered', request_method='GET',
58 58 renderer=None)
59 59 def render_email(self):
60 60 c = self.load_default_context()
61 61 email_id = self.request.matchdict['email_id']
62 62 c.active = 'emails'
63 63
64 64 pr = AttributeDict(
65 65 pull_request_id=123,
66 66 title='digital_ocean: fix redis, elastic search start on boot, '
67 67 'fix fd limits on supervisor, set postgres 11 version',
68 68 description='''
69 69 Check if we should use full-topic or mini-topic.
70 70
71 71 - full topic produces some problems with merge states etc
72 72 - server-mini-topic needs probably tweeks.
73 73 ''',
74 74 repo_name='foobar',
75 75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 77 )
78 78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
81 81
82 82 email_kwargs = {
83 83 'test': {},
84 84 'message': {
85 85 'body': 'message body !'
86 86 },
87 87 'email_test': {
88 88 'user': user,
89 89 'date': datetime.datetime.now(),
90 90 'rhodecode_version': c.rhodecode_version
91 91 },
92 92 'password_reset': {
93 93 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
94 94
95 95 'user': user,
96 96 'date': datetime.datetime.now(),
97 97 'email': 'test@rhodecode.com',
98 98 'first_admin_email': User.get_first_super_admin().email
99 99 },
100 100 'password_reset_confirmation': {
101 101 'new_password': 'new-password-example',
102 102 'user': user,
103 103 'date': datetime.datetime.now(),
104 104 'email': 'test@rhodecode.com',
105 105 'first_admin_email': User.get_first_super_admin().email
106 106 },
107 107 'registration': {
108 108 'user': user,
109 109 'date': datetime.datetime.now(),
110 110 },
111 111
112 112 'pull_request_comment': {
113 113 'user': user,
114 114
115 115 'status_change': None,
116 116 'status_change_type': None,
117 117
118 118 'pull_request': pr,
119 119 'pull_request_commits': [],
120 120
121 121 'pull_request_target_repo': target_repo,
122 122 'pull_request_target_repo_url': 'http://target-repo/url',
123 123
124 124 'pull_request_source_repo': source_repo,
125 125 'pull_request_source_repo_url': 'http://source-repo/url',
126 126
127 127 'pull_request_url': 'http://localhost/pr1',
128 128 'pr_comment_url': 'http://comment-url',
129 'pr_comment_reply_url': 'http://comment-url#reply',
129 130
130 131 'comment_file': None,
131 132 'comment_line': None,
132 133 'comment_type': 'note',
133 134 'comment_body': 'This is my comment body. *I like !*',
134
135 'comment_id': 2048,
135 136 'renderer_type': 'markdown',
136 137 'mention': True,
137 138
138 139 },
139 140 'pull_request_comment+status': {
140 141 'user': user,
141 142
142 143 'status_change': 'approved',
143 144 'status_change_type': 'approved',
144 145
145 146 'pull_request': pr,
146 147 'pull_request_commits': [],
147 148
148 149 'pull_request_target_repo': target_repo,
149 150 'pull_request_target_repo_url': 'http://target-repo/url',
150 151
151 152 'pull_request_source_repo': source_repo,
152 153 'pull_request_source_repo_url': 'http://source-repo/url',
153 154
154 155 'pull_request_url': 'http://localhost/pr1',
155 156 'pr_comment_url': 'http://comment-url',
157 'pr_comment_reply_url': 'http://comment-url#reply',
156 158
157 159 'comment_type': 'todo',
158 160 'comment_file': None,
159 161 'comment_line': None,
160 162 'comment_body': '''
161 163 I think something like this would be better
162 164
163 165 ```py
164 166
165 167 def db():
166 168 global connection
167 169 return connection
168 170
169 171 ```
170 172
171 173 ''',
172
174 'comment_id': 2048,
173 175 'renderer_type': 'markdown',
174 176 'mention': True,
175 177
176 178 },
177 179 'pull_request_comment+file': {
178 180 'user': user,
179 181
180 182 'status_change': None,
181 183 'status_change_type': None,
182 184
183 185 'pull_request': pr,
184 186 'pull_request_commits': [],
185 187
186 188 'pull_request_target_repo': target_repo,
187 189 'pull_request_target_repo_url': 'http://target-repo/url',
188 190
189 191 'pull_request_source_repo': source_repo,
190 192 'pull_request_source_repo_url': 'http://source-repo/url',
191 193
192 194 'pull_request_url': 'http://localhost/pr1',
193 195
194 196 'pr_comment_url': 'http://comment-url',
197 'pr_comment_reply_url': 'http://comment-url#reply',
195 198
196 199 'comment_file': 'rhodecode/model/db.py',
197 200 'comment_line': 'o1210',
198 201 'comment_type': 'todo',
199 202 'comment_body': '''
200 203 I like this !
201 204
202 205 But please check this code::
203 206
204 207 def main():
205 208 print 'ok'
206 209
207 210 This should work better !
208 211 ''',
209
212 'comment_id': 2048,
210 213 'renderer_type': 'rst',
211 214 'mention': True,
212 215
213 216 },
214 217
215 218 'cs_comment': {
216 219 'user': user,
217 220 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
218 221 'status_change': None,
219 222 'status_change_type': None,
220 223
221 224 'commit_target_repo_url': 'http://foo.example.com/#comment1',
222 225 'repo_name': 'test-repo',
223 226 'comment_type': 'note',
224 227 'comment_file': None,
225 228 'comment_line': None,
226 229 'commit_comment_url': 'http://comment-url',
230 'commit_comment_reply_url': 'http://comment-url#reply',
227 231 'comment_body': 'This is my comment body. *I like !*',
232 'comment_id': 2048,
228 233 'renderer_type': 'markdown',
229 234 'mention': True,
230 235 },
231 236 'cs_comment+status': {
232 237 'user': user,
233 238 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
234 239 'status_change': 'approved',
235 240 'status_change_type': 'approved',
236 241
237 242 'commit_target_repo_url': 'http://foo.example.com/#comment1',
238 243 'repo_name': 'test-repo',
239 244 'comment_type': 'note',
240 245 'comment_file': None,
241 246 'comment_line': None,
242 247 'commit_comment_url': 'http://comment-url',
248 'commit_comment_reply_url': 'http://comment-url#reply',
243 249 'comment_body': '''
244 250 Hello **world**
245 251
246 252 This is a multiline comment :)
247 253
248 254 - list
249 255 - list2
250 256 ''',
257 'comment_id': 2048,
251 258 'renderer_type': 'markdown',
252 259 'mention': True,
253 260 },
254 261 'cs_comment+file': {
255 262 'user': user,
256 263 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
257 264 'status_change': None,
258 265 'status_change_type': None,
259 266
260 267 'commit_target_repo_url': 'http://foo.example.com/#comment1',
261 268 'repo_name': 'test-repo',
262 269
263 270 'comment_type': 'note',
264 271 'comment_file': 'test-file.py',
265 272 'comment_line': 'n100',
266 273
267 274 'commit_comment_url': 'http://comment-url',
275 'commit_comment_reply_url': 'http://comment-url#reply',
268 276 'comment_body': 'This is my comment body. *I like !*',
277 'comment_id': 2048,
269 278 'renderer_type': 'markdown',
270 279 'mention': True,
271 280 },
272 281
273 282 'pull_request': {
274 283 'user': user,
275 284 'pull_request': pr,
276 285 'pull_request_commits': [
277 286 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
278 287 my-account: moved email closer to profile as it's similar data just moved outside.
279 288 '''),
280 289 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
281 290 users: description edit fixes
282 291
283 292 - tests
284 293 - added metatags info
285 294 '''),
286 295 ],
287 296
288 297 'pull_request_target_repo': target_repo,
289 298 'pull_request_target_repo_url': 'http://target-repo/url',
290 299
291 300 'pull_request_source_repo': source_repo,
292 301 'pull_request_source_repo_url': 'http://source-repo/url',
293 302
294 303 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
295 304 }
296 305
297 306 }
298 307
299 308 template_type = email_id.split('+')[0]
300 309 (c.subject, c.headers, c.email_body,
301 310 c.email_body_plaintext) = EmailNotificationModel().render_email(
302 311 template_type, **email_kwargs.get(email_id, {}))
303 312
304 313 test_email = self.request.GET.get('email')
305 314 if test_email:
306 315 recipients = [test_email]
307 316 run_task(tasks.send_email, recipients, c.subject,
308 317 c.email_body_plaintext, c.email_body)
309 318
310 319 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
311 320 template = 'debug_style/email_plain_rendered.mako'
312 321 else:
313 322 template = 'debug_style/email.mako'
314 323 return render_to_response(
315 324 template, self._get_template_context(c),
316 325 request=self.request)
317 326
318 327 @view_config(
319 328 route_name='debug_style_template', request_method='GET',
320 329 renderer=None)
321 330 def template(self):
322 331 t_path = self.request.matchdict['t_path']
323 332 c = self.load_default_context()
324 333 c.active = os.path.splitext(t_path)[0]
325 334 c.came_from = ''
326 335 c.email_types = {
327 336 'cs_comment+file': {},
328 337 'cs_comment+status': {},
329 338
330 339 'pull_request_comment+file': {},
331 340 'pull_request_comment+status': {},
332 341 }
333 342 c.email_types.update(EmailNotificationModel.email_types)
334 343
335 344 return render_to_response(
336 345 'debug_style/' + t_path, self._get_template_context(c),
337 346 request=self.request)
338 347
@@ -1,746 +1,754 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from pyramid.threadlocal import get_current_registry, get_current_request
30 30 from sqlalchemy.sql.expression import null
31 31 from sqlalchemy.sql.functions import coalesce
32 32
33 33 from rhodecode.lib import helpers as h, diffs, channelstream
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import (
38 38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 39 from rhodecode.model.notification import NotificationModel
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.settings import VcsSettingsModel
42 42 from rhodecode.model.notification import EmailNotificationModel
43 43 from rhodecode.model.validation_schema.schemas import comment_schema
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class CommentsModel(BaseModel):
50 50
51 51 cls = ChangesetComment
52 52
53 53 DIFF_CONTEXT_BEFORE = 3
54 54 DIFF_CONTEXT_AFTER = 3
55 55
56 56 def __get_commit_comment(self, changeset_comment):
57 57 return self._get_instance(ChangesetComment, changeset_comment)
58 58
59 59 def __get_pull_request(self, pull_request):
60 60 return self._get_instance(PullRequest, pull_request)
61 61
62 62 def _extract_mentions(self, s):
63 63 user_objects = []
64 64 for username in extract_mentioned_users(s):
65 65 user_obj = User.get_by_username(username, case_insensitive=True)
66 66 if user_obj:
67 67 user_objects.append(user_obj)
68 68 return user_objects
69 69
70 70 def _get_renderer(self, global_renderer='rst', request=None):
71 71 request = request or get_current_request()
72 72
73 73 try:
74 74 global_renderer = request.call_context.visual.default_renderer
75 75 except AttributeError:
76 76 log.debug("Renderer not set, falling back "
77 77 "to default renderer '%s'", global_renderer)
78 78 except Exception:
79 79 log.error(traceback.format_exc())
80 80 return global_renderer
81 81
82 82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 83 # group by versions, and count until, and display objects
84 84
85 85 comment_groups = collections.defaultdict(list)
86 86 [comment_groups[
87 87 _co.pull_request_version_id].append(_co) for _co in comments]
88 88
89 89 def yield_comments(pos):
90 90 for co in comment_groups[pos]:
91 91 yield co
92 92
93 93 comment_versions = collections.defaultdict(
94 94 lambda: collections.defaultdict(list))
95 95 prev_prvid = -1
96 96 # fake last entry with None, to aggregate on "latest" version which
97 97 # doesn't have an pull_request_version_id
98 98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 99 prvid = ver.pull_request_version_id
100 100 if prev_prvid == -1:
101 101 prev_prvid = prvid
102 102
103 103 for co in yield_comments(prvid):
104 104 comment_versions[prvid]['at'].append(co)
105 105
106 106 # save until
107 107 current = comment_versions[prvid]['at']
108 108 prev_until = comment_versions[prev_prvid]['until']
109 109 cur_until = prev_until + current
110 110 comment_versions[prvid]['until'].extend(cur_until)
111 111
112 112 # save outdated
113 113 if inline:
114 114 outdated = [x for x in cur_until
115 115 if x.outdated_at_version(show_version)]
116 116 else:
117 117 outdated = [x for x in cur_until
118 118 if x.older_than_version(show_version)]
119 119 display = [x for x in cur_until if x not in outdated]
120 120
121 121 comment_versions[prvid]['outdated'] = outdated
122 122 comment_versions[prvid]['display'] = display
123 123
124 124 prev_prvid = prvid
125 125
126 126 return comment_versions
127 127
128 128 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
129 129 qry = Session().query(ChangesetComment) \
130 130 .filter(ChangesetComment.repo == repo)
131 131
132 132 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
133 133 qry = qry.filter(ChangesetComment.comment_type == comment_type)
134 134
135 135 if user:
136 136 user = self._get_user(user)
137 137 if user:
138 138 qry = qry.filter(ChangesetComment.user_id == user.user_id)
139 139
140 140 if commit_id:
141 141 qry = qry.filter(ChangesetComment.revision == commit_id)
142 142
143 143 qry = qry.order_by(ChangesetComment.created_on)
144 144 return qry.all()
145 145
146 146 def get_repository_unresolved_todos(self, repo):
147 147 todos = Session().query(ChangesetComment) \
148 148 .filter(ChangesetComment.repo == repo) \
149 149 .filter(ChangesetComment.resolved_by == None) \
150 150 .filter(ChangesetComment.comment_type
151 151 == ChangesetComment.COMMENT_TYPE_TODO)
152 152 todos = todos.all()
153 153
154 154 return todos
155 155
156 156 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
157 157
158 158 todos = Session().query(ChangesetComment) \
159 159 .filter(ChangesetComment.pull_request == pull_request) \
160 160 .filter(ChangesetComment.resolved_by == None) \
161 161 .filter(ChangesetComment.comment_type
162 162 == ChangesetComment.COMMENT_TYPE_TODO)
163 163
164 164 if not show_outdated:
165 165 todos = todos.filter(
166 166 coalesce(ChangesetComment.display_state, '') !=
167 167 ChangesetComment.COMMENT_OUTDATED)
168 168
169 169 todos = todos.all()
170 170
171 171 return todos
172 172
173 173 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
174 174
175 175 todos = Session().query(ChangesetComment) \
176 176 .filter(ChangesetComment.pull_request == pull_request) \
177 177 .filter(ChangesetComment.resolved_by != None) \
178 178 .filter(ChangesetComment.comment_type
179 179 == ChangesetComment.COMMENT_TYPE_TODO)
180 180
181 181 if not show_outdated:
182 182 todos = todos.filter(
183 183 coalesce(ChangesetComment.display_state, '') !=
184 184 ChangesetComment.COMMENT_OUTDATED)
185 185
186 186 todos = todos.all()
187 187
188 188 return todos
189 189
190 190 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
191 191
192 192 todos = Session().query(ChangesetComment) \
193 193 .filter(ChangesetComment.revision == commit_id) \
194 194 .filter(ChangesetComment.resolved_by == None) \
195 195 .filter(ChangesetComment.comment_type
196 196 == ChangesetComment.COMMENT_TYPE_TODO)
197 197
198 198 if not show_outdated:
199 199 todos = todos.filter(
200 200 coalesce(ChangesetComment.display_state, '') !=
201 201 ChangesetComment.COMMENT_OUTDATED)
202 202
203 203 todos = todos.all()
204 204
205 205 return todos
206 206
207 207 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
208 208
209 209 todos = Session().query(ChangesetComment) \
210 210 .filter(ChangesetComment.revision == commit_id) \
211 211 .filter(ChangesetComment.resolved_by != None) \
212 212 .filter(ChangesetComment.comment_type
213 213 == ChangesetComment.COMMENT_TYPE_TODO)
214 214
215 215 if not show_outdated:
216 216 todos = todos.filter(
217 217 coalesce(ChangesetComment.display_state, '') !=
218 218 ChangesetComment.COMMENT_OUTDATED)
219 219
220 220 todos = todos.all()
221 221
222 222 return todos
223 223
224 224 def _log_audit_action(self, action, action_data, auth_user, comment):
225 225 audit_logger.store(
226 226 action=action,
227 227 action_data=action_data,
228 228 user=auth_user,
229 229 repo=comment.repo)
230 230
231 231 def create(self, text, repo, user, commit_id=None, pull_request=None,
232 232 f_path=None, line_no=None, status_change=None,
233 233 status_change_type=None, comment_type=None,
234 234 resolves_comment_id=None, closing_pr=False, send_email=True,
235 235 renderer=None, auth_user=None, extra_recipients=None):
236 236 """
237 237 Creates new comment for commit or pull request.
238 238 IF status_change is not none this comment is associated with a
239 239 status change of commit or commit associated with pull request
240 240
241 241 :param text:
242 242 :param repo:
243 243 :param user:
244 244 :param commit_id:
245 245 :param pull_request:
246 246 :param f_path:
247 247 :param line_no:
248 248 :param status_change: Label for status change
249 249 :param comment_type: Type of comment
250 250 :param resolves_comment_id: id of comment which this one will resolve
251 251 :param status_change_type: type of status change
252 252 :param closing_pr:
253 253 :param send_email:
254 254 :param renderer: pick renderer for this comment
255 255 :param auth_user: current authenticated user calling this method
256 256 :param extra_recipients: list of extra users to be added to recipients
257 257 """
258 258
259 259 if not text:
260 260 log.warning('Missing text for comment, skipping...')
261 261 return
262 262 request = get_current_request()
263 263 _ = request.translate
264 264
265 265 if not renderer:
266 266 renderer = self._get_renderer(request=request)
267 267
268 268 repo = self._get_repo(repo)
269 269 user = self._get_user(user)
270 270 auth_user = auth_user or user
271 271
272 272 schema = comment_schema.CommentSchema()
273 273 validated_kwargs = schema.deserialize(dict(
274 274 comment_body=text,
275 275 comment_type=comment_type,
276 276 comment_file=f_path,
277 277 comment_line=line_no,
278 278 renderer_type=renderer,
279 279 status_change=status_change_type,
280 280 resolves_comment_id=resolves_comment_id,
281 281 repo=repo.repo_id,
282 282 user=user.user_id,
283 283 ))
284 284
285 285 comment = ChangesetComment()
286 286 comment.renderer = validated_kwargs['renderer_type']
287 287 comment.text = validated_kwargs['comment_body']
288 288 comment.f_path = validated_kwargs['comment_file']
289 289 comment.line_no = validated_kwargs['comment_line']
290 290 comment.comment_type = validated_kwargs['comment_type']
291 291
292 292 comment.repo = repo
293 293 comment.author = user
294 294 resolved_comment = self.__get_commit_comment(
295 295 validated_kwargs['resolves_comment_id'])
296 296 # check if the comment actually belongs to this PR
297 297 if resolved_comment and resolved_comment.pull_request and \
298 298 resolved_comment.pull_request != pull_request:
299 299 log.warning('Comment tried to resolved unrelated todo comment: %s',
300 300 resolved_comment)
301 301 # comment not bound to this pull request, forbid
302 302 resolved_comment = None
303 303
304 304 elif resolved_comment and resolved_comment.repo and \
305 305 resolved_comment.repo != repo:
306 306 log.warning('Comment tried to resolved unrelated todo comment: %s',
307 307 resolved_comment)
308 308 # comment not bound to this repo, forbid
309 309 resolved_comment = None
310 310
311 311 comment.resolved_comment = resolved_comment
312 312
313 313 pull_request_id = pull_request
314 314
315 315 commit_obj = None
316 316 pull_request_obj = None
317 317
318 318 if commit_id:
319 319 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
320 320 # do a lookup, so we don't pass something bad here
321 321 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
322 322 comment.revision = commit_obj.raw_id
323 323
324 324 elif pull_request_id:
325 325 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
326 326 pull_request_obj = self.__get_pull_request(pull_request_id)
327 327 comment.pull_request = pull_request_obj
328 328 else:
329 329 raise Exception('Please specify commit or pull_request_id')
330 330
331 331 Session().add(comment)
332 332 Session().flush()
333 333 kwargs = {
334 334 'user': user,
335 335 'renderer_type': renderer,
336 336 'repo_name': repo.repo_name,
337 337 'status_change': status_change,
338 338 'status_change_type': status_change_type,
339 339 'comment_body': text,
340 340 'comment_file': f_path,
341 341 'comment_line': line_no,
342 'comment_type': comment_type or 'note'
342 'comment_type': comment_type or 'note',
343 'comment_id': comment.comment_id
343 344 }
344 345
345 346 if commit_obj:
346 347 recipients = ChangesetComment.get_users(
347 348 revision=commit_obj.raw_id)
348 349 # add commit author if it's in RhodeCode system
349 350 cs_author = User.get_from_cs_author(commit_obj.author)
350 351 if not cs_author:
351 352 # use repo owner if we cannot extract the author correctly
352 353 cs_author = repo.user
353 354 recipients += [cs_author]
354 355
355 356 commit_comment_url = self.get_url(comment, request=request)
357 commit_comment_reply_url = self.get_url(
358 comment, request=request,
359 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
356 360
357 361 target_repo_url = h.link_to(
358 362 repo.repo_name,
359 363 h.route_url('repo_summary', repo_name=repo.repo_name))
360 364
361 365 # commit specifics
362 366 kwargs.update({
363 367 'commit': commit_obj,
364 368 'commit_message': commit_obj.message,
365 369 'commit_target_repo_url': target_repo_url,
366 370 'commit_comment_url': commit_comment_url,
371 'commit_comment_reply_url': commit_comment_reply_url
367 372 })
368 373
369 374 elif pull_request_obj:
370 375 # get the current participants of this pull request
371 376 recipients = ChangesetComment.get_users(
372 377 pull_request_id=pull_request_obj.pull_request_id)
373 378 # add pull request author
374 379 recipients += [pull_request_obj.author]
375 380
376 381 # add the reviewers to notification
377 382 recipients += [x.user for x in pull_request_obj.reviewers]
378 383
379 384 pr_target_repo = pull_request_obj.target_repo
380 385 pr_source_repo = pull_request_obj.source_repo
381 386
382 pr_comment_url = h.route_url(
383 'pullrequest_show',
384 repo_name=pr_target_repo.repo_name,
385 pull_request_id=pull_request_obj.pull_request_id,
386 _anchor='comment-%s' % comment.comment_id)
387 pr_comment_url = self.get_url(comment, request=request)
388 pr_comment_reply_url = self.get_url(
389 comment, request=request,
390 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
387 391
388 392 pr_url = h.route_url(
389 393 'pullrequest_show',
390 394 repo_name=pr_target_repo.repo_name,
391 395 pull_request_id=pull_request_obj.pull_request_id, )
392 396
393 397 # set some variables for email notification
394 398 pr_target_repo_url = h.route_url(
395 399 'repo_summary', repo_name=pr_target_repo.repo_name)
396 400
397 401 pr_source_repo_url = h.route_url(
398 402 'repo_summary', repo_name=pr_source_repo.repo_name)
399 403
400 404 # pull request specifics
401 405 kwargs.update({
402 406 'pull_request': pull_request_obj,
403 407 'pr_id': pull_request_obj.pull_request_id,
404 408 'pull_request_url': pr_url,
405 409 'pull_request_target_repo': pr_target_repo,
406 410 'pull_request_target_repo_url': pr_target_repo_url,
407 411 'pull_request_source_repo': pr_source_repo,
408 412 'pull_request_source_repo_url': pr_source_repo_url,
409 413 'pr_comment_url': pr_comment_url,
414 'pr_comment_reply_url': pr_comment_reply_url,
410 415 'pr_closing': closing_pr,
411 416 })
412 417
413 418 recipients += [self._get_user(u) for u in (extra_recipients or [])]
414 419
415 420 if send_email:
416 421 # pre-generate the subject for notification itself
417 422 (subject,
418 423 _h, _e, # we don't care about those
419 424 body_plaintext) = EmailNotificationModel().render_email(
420 425 notification_type, **kwargs)
421 426
422 427 mention_recipients = set(
423 428 self._extract_mentions(text)).difference(recipients)
424 429
425 430 # create notification objects, and emails
426 431 NotificationModel().create(
427 432 created_by=user,
428 433 notification_subject=subject,
429 434 notification_body=body_plaintext,
430 435 notification_type=notification_type,
431 436 recipients=recipients,
432 437 mention_recipients=mention_recipients,
433 438 email_kwargs=kwargs,
434 439 )
435 440
436 441 Session().flush()
437 442 if comment.pull_request:
438 443 action = 'repo.pull_request.comment.create'
439 444 else:
440 445 action = 'repo.commit.comment.create'
441 446
442 447 comment_data = comment.get_api_data()
443 448 self._log_audit_action(
444 449 action, {'data': comment_data}, auth_user, comment)
445 450
446 451 msg_url = ''
447 452 channel = None
448 453 if commit_obj:
449 454 msg_url = commit_comment_url
450 455 repo_name = repo.repo_name
451 456 channel = u'/repo${}$/commit/{}'.format(
452 457 repo_name,
453 458 commit_obj.raw_id
454 459 )
455 460 elif pull_request_obj:
456 461 msg_url = pr_comment_url
457 462 repo_name = pr_target_repo.repo_name
458 463 channel = u'/repo${}$/pr/{}'.format(
459 464 repo_name,
460 465 pull_request_id
461 466 )
462 467
463 468 message = '<strong>{}</strong> {} - ' \
464 469 '<a onclick="window.location=\'{}\';' \
465 470 'window.location.reload()">' \
466 471 '<strong>{}</strong></a>'
467 472 message = message.format(
468 473 user.username, _('made a comment'), msg_url,
469 474 _('Show it now'))
470 475
471 476 channelstream.post_message(
472 477 channel, message, user.username,
473 478 registry=get_current_registry())
474 479
475 480 return comment
476 481
477 482 def delete(self, comment, auth_user):
478 483 """
479 484 Deletes given comment
480 485 """
481 486 comment = self.__get_commit_comment(comment)
482 487 old_data = comment.get_api_data()
483 488 Session().delete(comment)
484 489
485 490 if comment.pull_request:
486 491 action = 'repo.pull_request.comment.delete'
487 492 else:
488 493 action = 'repo.commit.comment.delete'
489 494
490 495 self._log_audit_action(
491 496 action, {'old_data': old_data}, auth_user, comment)
492 497
493 498 return comment
494 499
495 500 def get_all_comments(self, repo_id, revision=None, pull_request=None):
496 501 q = ChangesetComment.query()\
497 502 .filter(ChangesetComment.repo_id == repo_id)
498 503 if revision:
499 504 q = q.filter(ChangesetComment.revision == revision)
500 505 elif pull_request:
501 506 pull_request = self.__get_pull_request(pull_request)
502 507 q = q.filter(ChangesetComment.pull_request == pull_request)
503 508 else:
504 509 raise Exception('Please specify commit or pull_request')
505 510 q = q.order_by(ChangesetComment.created_on)
506 511 return q.all()
507 512
508 def get_url(self, comment, request=None, permalink=False):
513 def get_url(self, comment, request=None, permalink=False, anchor=None):
509 514 if not request:
510 515 request = get_current_request()
511 516
512 517 comment = self.__get_commit_comment(comment)
518 if anchor is None:
519 anchor = 'comment-{}'.format(comment.comment_id)
520
513 521 if comment.pull_request:
514 522 pull_request = comment.pull_request
515 523 if permalink:
516 524 return request.route_url(
517 525 'pull_requests_global',
518 526 pull_request_id=pull_request.pull_request_id,
519 _anchor='comment-%s' % comment.comment_id)
527 _anchor=anchor)
520 528 else:
521 529 return request.route_url(
522 530 'pullrequest_show',
523 531 repo_name=safe_str(pull_request.target_repo.repo_name),
524 532 pull_request_id=pull_request.pull_request_id,
525 _anchor='comment-%s' % comment.comment_id)
533 _anchor=anchor)
526 534
527 535 else:
528 536 repo = comment.repo
529 537 commit_id = comment.revision
530 538
531 539 if permalink:
532 540 return request.route_url(
533 541 'repo_commit', repo_name=safe_str(repo.repo_id),
534 542 commit_id=commit_id,
535 _anchor='comment-%s' % comment.comment_id)
543 _anchor=anchor)
536 544
537 545 else:
538 546 return request.route_url(
539 547 'repo_commit', repo_name=safe_str(repo.repo_name),
540 548 commit_id=commit_id,
541 _anchor='comment-%s' % comment.comment_id)
549 _anchor=anchor)
542 550
543 551 def get_comments(self, repo_id, revision=None, pull_request=None):
544 552 """
545 553 Gets main comments based on revision or pull_request_id
546 554
547 555 :param repo_id:
548 556 :param revision:
549 557 :param pull_request:
550 558 """
551 559
552 560 q = ChangesetComment.query()\
553 561 .filter(ChangesetComment.repo_id == repo_id)\
554 562 .filter(ChangesetComment.line_no == None)\
555 563 .filter(ChangesetComment.f_path == None)
556 564 if revision:
557 565 q = q.filter(ChangesetComment.revision == revision)
558 566 elif pull_request:
559 567 pull_request = self.__get_pull_request(pull_request)
560 568 q = q.filter(ChangesetComment.pull_request == pull_request)
561 569 else:
562 570 raise Exception('Please specify commit or pull_request')
563 571 q = q.order_by(ChangesetComment.created_on)
564 572 return q.all()
565 573
566 574 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
567 575 q = self._get_inline_comments_query(repo_id, revision, pull_request)
568 576 return self._group_comments_by_path_and_line_number(q)
569 577
570 578 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
571 579 version=None):
572 580 inline_cnt = 0
573 581 for fname, per_line_comments in inline_comments.iteritems():
574 582 for lno, comments in per_line_comments.iteritems():
575 583 for comm in comments:
576 584 if not comm.outdated_at_version(version) and skip_outdated:
577 585 inline_cnt += 1
578 586
579 587 return inline_cnt
580 588
581 589 def get_outdated_comments(self, repo_id, pull_request):
582 590 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
583 591 # of a pull request.
584 592 q = self._all_inline_comments_of_pull_request(pull_request)
585 593 q = q.filter(
586 594 ChangesetComment.display_state ==
587 595 ChangesetComment.COMMENT_OUTDATED
588 596 ).order_by(ChangesetComment.comment_id.asc())
589 597
590 598 return self._group_comments_by_path_and_line_number(q)
591 599
592 600 def _get_inline_comments_query(self, repo_id, revision, pull_request):
593 601 # TODO: johbo: Split this into two methods: One for PR and one for
594 602 # commit.
595 603 if revision:
596 604 q = Session().query(ChangesetComment).filter(
597 605 ChangesetComment.repo_id == repo_id,
598 606 ChangesetComment.line_no != null(),
599 607 ChangesetComment.f_path != null(),
600 608 ChangesetComment.revision == revision)
601 609
602 610 elif pull_request:
603 611 pull_request = self.__get_pull_request(pull_request)
604 612 if not CommentsModel.use_outdated_comments(pull_request):
605 613 q = self._visible_inline_comments_of_pull_request(pull_request)
606 614 else:
607 615 q = self._all_inline_comments_of_pull_request(pull_request)
608 616
609 617 else:
610 618 raise Exception('Please specify commit or pull_request_id')
611 619 q = q.order_by(ChangesetComment.comment_id.asc())
612 620 return q
613 621
614 622 def _group_comments_by_path_and_line_number(self, q):
615 623 comments = q.all()
616 624 paths = collections.defaultdict(lambda: collections.defaultdict(list))
617 625 for co in comments:
618 626 paths[co.f_path][co.line_no].append(co)
619 627 return paths
620 628
621 629 @classmethod
622 630 def needed_extra_diff_context(cls):
623 631 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
624 632
625 633 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
626 634 if not CommentsModel.use_outdated_comments(pull_request):
627 635 return
628 636
629 637 comments = self._visible_inline_comments_of_pull_request(pull_request)
630 638 comments_to_outdate = comments.all()
631 639
632 640 for comment in comments_to_outdate:
633 641 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
634 642
635 643 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
636 644 diff_line = _parse_comment_line_number(comment.line_no)
637 645
638 646 try:
639 647 old_context = old_diff_proc.get_context_of_line(
640 648 path=comment.f_path, diff_line=diff_line)
641 649 new_context = new_diff_proc.get_context_of_line(
642 650 path=comment.f_path, diff_line=diff_line)
643 651 except (diffs.LineNotInDiffException,
644 652 diffs.FileNotInDiffException):
645 653 comment.display_state = ChangesetComment.COMMENT_OUTDATED
646 654 return
647 655
648 656 if old_context == new_context:
649 657 return
650 658
651 659 if self._should_relocate_diff_line(diff_line):
652 660 new_diff_lines = new_diff_proc.find_context(
653 661 path=comment.f_path, context=old_context,
654 662 offset=self.DIFF_CONTEXT_BEFORE)
655 663 if not new_diff_lines:
656 664 comment.display_state = ChangesetComment.COMMENT_OUTDATED
657 665 else:
658 666 new_diff_line = self._choose_closest_diff_line(
659 667 diff_line, new_diff_lines)
660 668 comment.line_no = _diff_to_comment_line_number(new_diff_line)
661 669 else:
662 670 comment.display_state = ChangesetComment.COMMENT_OUTDATED
663 671
664 672 def _should_relocate_diff_line(self, diff_line):
665 673 """
666 674 Checks if relocation shall be tried for the given `diff_line`.
667 675
668 676 If a comment points into the first lines, then we can have a situation
669 677 that after an update another line has been added on top. In this case
670 678 we would find the context still and move the comment around. This
671 679 would be wrong.
672 680 """
673 681 should_relocate = (
674 682 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
675 683 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
676 684 return should_relocate
677 685
678 686 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
679 687 candidate = new_diff_lines[0]
680 688 best_delta = _diff_line_delta(diff_line, candidate)
681 689 for new_diff_line in new_diff_lines[1:]:
682 690 delta = _diff_line_delta(diff_line, new_diff_line)
683 691 if delta < best_delta:
684 692 candidate = new_diff_line
685 693 best_delta = delta
686 694 return candidate
687 695
688 696 def _visible_inline_comments_of_pull_request(self, pull_request):
689 697 comments = self._all_inline_comments_of_pull_request(pull_request)
690 698 comments = comments.filter(
691 699 coalesce(ChangesetComment.display_state, '') !=
692 700 ChangesetComment.COMMENT_OUTDATED)
693 701 return comments
694 702
695 703 def _all_inline_comments_of_pull_request(self, pull_request):
696 704 comments = Session().query(ChangesetComment)\
697 705 .filter(ChangesetComment.line_no != None)\
698 706 .filter(ChangesetComment.f_path != None)\
699 707 .filter(ChangesetComment.pull_request == pull_request)
700 708 return comments
701 709
702 710 def _all_general_comments_of_pull_request(self, pull_request):
703 711 comments = Session().query(ChangesetComment)\
704 712 .filter(ChangesetComment.line_no == None)\
705 713 .filter(ChangesetComment.f_path == None)\
706 714 .filter(ChangesetComment.pull_request == pull_request)
707 715 return comments
708 716
709 717 @staticmethod
710 718 def use_outdated_comments(pull_request):
711 719 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
712 720 settings = settings_model.get_general_settings()
713 721 return settings.get('rhodecode_use_outdated_comments', False)
714 722
715 723
716 724 def _parse_comment_line_number(line_no):
717 725 """
718 726 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
719 727 """
720 728 old_line = None
721 729 new_line = None
722 730 if line_no.startswith('o'):
723 731 old_line = int(line_no[1:])
724 732 elif line_no.startswith('n'):
725 733 new_line = int(line_no[1:])
726 734 else:
727 735 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
728 736 return diffs.DiffLineNumber(old_line, new_line)
729 737
730 738
731 739 def _diff_to_comment_line_number(diff_line):
732 740 if diff_line.new is not None:
733 741 return u'n{}'.format(diff_line.new)
734 742 elif diff_line.old is not None:
735 743 return u'o{}'.format(diff_line.old)
736 744 return u''
737 745
738 746
739 747 def _diff_line_delta(a, b):
740 748 if None not in (a.new, b.new):
741 749 return abs(a.new - b.new)
742 750 elif None not in (a.old, b.old):
743 751 return abs(a.old - b.old)
744 752 else:
745 753 raise ValueError(
746 754 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1289 +1,1293 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21
22 22 div.diffblock .sidebyside {
23 23 background: #ffffff;
24 24 }
25 25
26 26 div.diffblock {
27 27 overflow-x: auto;
28 28 overflow-y: hidden;
29 29 clear: both;
30 30 padding: 0px;
31 31 background: @grey6;
32 32 border: @border-thickness solid @grey5;
33 33 -webkit-border-radius: @border-radius @border-radius 0px 0px;
34 34 border-radius: @border-radius @border-radius 0px 0px;
35 35
36 36
37 37 .comments-number {
38 38 float: right;
39 39 }
40 40
41 41 // BEGIN CODE-HEADER STYLES
42 42
43 43 .code-header {
44 44 background: @grey6;
45 45 padding: 10px 0 10px 0;
46 46 height: auto;
47 47 width: 100%;
48 48
49 49 .hash {
50 50 float: left;
51 51 padding: 2px 0 0 2px;
52 52 }
53 53
54 54 .date {
55 55 float: left;
56 56 text-transform: uppercase;
57 57 padding: 4px 0px 0px 2px;
58 58 }
59 59
60 60 div {
61 61 margin-left: 4px;
62 62 }
63 63
64 64 div.compare_header {
65 65 min-height: 40px;
66 66 margin: 0;
67 67 padding: 0 @padding;
68 68
69 69 .drop-menu {
70 70 float:left;
71 71 display: block;
72 72 margin:0 0 @padding 0;
73 73 }
74 74
75 75 .compare-label {
76 76 float: left;
77 77 clear: both;
78 78 display: inline-block;
79 79 min-width: 5em;
80 80 margin: 0;
81 81 padding: @button-padding @button-padding @button-padding 0;
82 82 font-weight: @text-semibold-weight;
83 83 font-family: @text-semibold;
84 84 }
85 85
86 86 .compare-buttons {
87 87 float: left;
88 88 margin: 0;
89 89 padding: 0 0 @padding;
90 90
91 91 .btn {
92 92 margin: 0 @padding 0 0;
93 93 }
94 94 }
95 95 }
96 96
97 97 }
98 98
99 99 .parents {
100 100 float: left;
101 101 width: 100px;
102 102 font-weight: 400;
103 103 vertical-align: middle;
104 104 padding: 0px 2px 0px 2px;
105 105 background-color: @grey6;
106 106
107 107 #parent_link {
108 108 margin: 00px 2px;
109 109
110 110 &.double {
111 111 margin: 0px 2px;
112 112 }
113 113
114 114 &.disabled{
115 115 margin-right: @padding;
116 116 }
117 117 }
118 118 }
119 119
120 120 .children {
121 121 float: right;
122 122 width: 100px;
123 123 font-weight: 400;
124 124 vertical-align: middle;
125 125 text-align: right;
126 126 padding: 0px 2px 0px 2px;
127 127 background-color: @grey6;
128 128
129 129 #child_link {
130 130 margin: 0px 2px;
131 131
132 132 &.double {
133 133 margin: 0px 2px;
134 134 }
135 135
136 136 &.disabled{
137 137 margin-right: @padding;
138 138 }
139 139 }
140 140 }
141 141
142 142 .changeset_header {
143 143 height: 16px;
144 144
145 145 & > div{
146 146 margin-right: @padding;
147 147 }
148 148 }
149 149
150 150 .changeset_file {
151 151 text-align: left;
152 152 float: left;
153 153 padding: 0;
154 154
155 155 a{
156 156 display: inline-block;
157 157 margin-right: 0.5em;
158 158 }
159 159
160 160 #selected_mode{
161 161 margin-left: 0;
162 162 }
163 163 }
164 164
165 165 .diff-menu-wrapper {
166 166 float: left;
167 167 }
168 168
169 169 .diff-menu {
170 170 position: absolute;
171 171 background: none repeat scroll 0 0 #FFFFFF;
172 172 border-color: #003367 @grey3 @grey3;
173 173 border-right: 1px solid @grey3;
174 174 border-style: solid solid solid;
175 175 border-width: @border-thickness;
176 176 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
177 177 margin-top: 5px;
178 178 margin-left: 1px;
179 179 }
180 180
181 181 .diff-actions, .editor-actions {
182 182 float: left;
183 183
184 184 input{
185 185 margin: 0 0.5em 0 0;
186 186 }
187 187 }
188 188
189 189 // END CODE-HEADER STYLES
190 190
191 191 // BEGIN CODE-BODY STYLES
192 192
193 193 .code-body {
194 194 padding: 0;
195 195 background-color: #ffffff;
196 196 position: relative;
197 197 max-width: none;
198 198 box-sizing: border-box;
199 199 // TODO: johbo: Parent has overflow: auto, this forces the child here
200 200 // to have the intended size and to scroll. Should be simplified.
201 201 width: 100%;
202 202 overflow-x: auto;
203 203 }
204 204
205 205 pre.raw {
206 206 background: white;
207 207 color: @grey1;
208 208 }
209 209 // END CODE-BODY STYLES
210 210
211 211 }
212 212
213 213
214 214 table.code-difftable {
215 215 border-collapse: collapse;
216 216 width: 99%;
217 217 border-radius: 0px !important;
218 218
219 219 td {
220 220 padding: 0 !important;
221 221 background: none !important;
222 222 border: 0 !important;
223 223 }
224 224
225 225 .context {
226 226 background: none repeat scroll 0 0 #DDE7EF;
227 227 }
228 228
229 229 .add {
230 230 background: none repeat scroll 0 0 #DDFFDD;
231 231
232 232 ins {
233 233 background: none repeat scroll 0 0 #AAFFAA;
234 234 text-decoration: none;
235 235 }
236 236 }
237 237
238 238 .del {
239 239 background: none repeat scroll 0 0 #FFDDDD;
240 240
241 241 del {
242 242 background: none repeat scroll 0 0 #FFAAAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 /** LINE NUMBERS **/
248 248 .lineno {
249 249 padding-left: 2px !important;
250 250 padding-right: 2px;
251 251 text-align: right;
252 252 width: 32px;
253 253 -moz-user-select: none;
254 254 -webkit-user-select: none;
255 255 border-right: @border-thickness solid @grey5 !important;
256 256 border-left: 0px solid #CCC !important;
257 257 border-top: 0px solid #CCC !important;
258 258 border-bottom: none !important;
259 259
260 260 a {
261 261 &:extend(pre);
262 262 text-align: right;
263 263 padding-right: 2px;
264 264 cursor: pointer;
265 265 display: block;
266 266 width: 32px;
267 267 }
268 268 }
269 269
270 270 .context {
271 271 cursor: auto;
272 272 &:extend(pre);
273 273 }
274 274
275 275 .lineno-inline {
276 276 background: none repeat scroll 0 0 #FFF !important;
277 277 padding-left: 2px;
278 278 padding-right: 2px;
279 279 text-align: right;
280 280 width: 30px;
281 281 -moz-user-select: none;
282 282 -webkit-user-select: none;
283 283 }
284 284
285 285 /** CODE **/
286 286 .code {
287 287 display: block;
288 288 width: 100%;
289 289
290 290 td {
291 291 margin: 0;
292 292 padding: 0;
293 293 }
294 294
295 295 pre {
296 296 margin: 0;
297 297 padding: 0;
298 298 margin-left: .5em;
299 299 }
300 300 }
301 301 }
302 302
303 303
304 304 // Comments
305
306 div.comment:target {
305 .comment-selected-hl {
307 306 border-left: 6px solid @comment-highlight-color !important;
308 padding-left: 3px;
309 margin-left: -9px;
307 padding-left: 3px !important;
308 margin-left: -7px !important;
309 }
310
311 div.comment:target,
312 div.comment-outdated:target {
313 .comment-selected-hl;
310 314 }
311 315
312 316 //TODO: anderson: can't get an absolute number out of anything, so had to put the
313 317 //current values that might change. But to make it clear I put as a calculation
314 318 @comment-max-width: 1065px;
315 319 @pr-extra-margin: 34px;
316 320 @pr-border-spacing: 4px;
317 321 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
318 322
319 323 // Pull Request
320 324 .cs_files .code-difftable {
321 325 border: @border-thickness solid @grey5; //borders only on PRs
322 326
323 327 .comment-inline-form,
324 328 div.comment {
325 329 width: @pr-comment-width;
326 330 }
327 331 }
328 332
329 333 // Changeset
330 334 .code-difftable {
331 335 .comment-inline-form,
332 336 div.comment {
333 337 width: @comment-max-width;
334 338 }
335 339 }
336 340
337 341 //Style page
338 342 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
339 343 #style-page .code-difftable{
340 344 .comment-inline-form,
341 345 div.comment {
342 346 width: @comment-max-width - @style-extra-margin;
343 347 }
344 348 }
345 349
346 350 #context-bar > h2 {
347 351 font-size: 20px;
348 352 }
349 353
350 354 #context-bar > h2> a {
351 355 font-size: 20px;
352 356 }
353 357 // end of defaults
354 358
355 359 .file_diff_buttons {
356 360 padding: 0 0 @padding;
357 361
358 362 .drop-menu {
359 363 float: left;
360 364 margin: 0 @padding 0 0;
361 365 }
362 366 .btn {
363 367 margin: 0 @padding 0 0;
364 368 }
365 369 }
366 370
367 371 .code-body.textarea.editor {
368 372 max-width: none;
369 373 padding: 15px;
370 374 }
371 375
372 376 td.injected_diff{
373 377 max-width: 1178px;
374 378 overflow-x: auto;
375 379 overflow-y: hidden;
376 380
377 381 div.diff-container,
378 382 div.diffblock{
379 383 max-width: 100%;
380 384 }
381 385
382 386 div.code-body {
383 387 max-width: 1124px;
384 388 overflow-x: auto;
385 389 overflow-y: hidden;
386 390 padding: 0;
387 391 }
388 392 div.diffblock {
389 393 border: none;
390 394 }
391 395
392 396 &.inline-form {
393 397 width: 99%
394 398 }
395 399 }
396 400
397 401
398 402 table.code-difftable {
399 403 width: 100%;
400 404 }
401 405
402 406 /** PYGMENTS COLORING **/
403 407 div.codeblock {
404 408
405 409 // TODO: johbo: Added interim to get rid of the margin around
406 410 // Select2 widgets. This needs further cleanup.
407 411 overflow: auto;
408 412 padding: 0px;
409 413 border: @border-thickness solid @grey6;
410 414 .border-radius(@border-radius);
411 415
412 416 #remove_gist {
413 417 float: right;
414 418 }
415 419
416 420 .gist_url {
417 421 padding: 0px 0px 10px 0px;
418 422 }
419 423
420 424 .author {
421 425 clear: both;
422 426 vertical-align: middle;
423 427 font-weight: @text-bold-weight;
424 428 font-family: @text-bold;
425 429 }
426 430
427 431 .btn-mini {
428 432 float: left;
429 433 margin: 0 5px 0 0;
430 434 }
431 435
432 436 .code-header {
433 437 padding: @padding;
434 438 border-bottom: @border-thickness solid @grey5;
435 439
436 440 .rc-user {
437 441 min-width: 0;
438 442 margin-right: .5em;
439 443 }
440 444
441 445 .stats {
442 446 clear: both;
443 447 margin: 0 0 @padding 0;
444 448 padding: 0;
445 449 .left {
446 450 float: left;
447 451 clear: left;
448 452 max-width: 75%;
449 453 margin: 0 0 @padding 0;
450 454
451 455 &.item {
452 456 margin-right: @padding;
453 457 &.last { border-right: none; }
454 458 }
455 459 }
456 460 .buttons { float: right; }
457 461 .author {
458 462 height: 25px; margin-left: 15px; font-weight: bold;
459 463 }
460 464 }
461 465
462 466 .commit {
463 467 margin: 5px 0 0 26px;
464 468 font-weight: normal;
465 469 white-space: pre-wrap;
466 470 }
467 471 }
468 472
469 473 .message {
470 474 position: relative;
471 475 margin: @padding;
472 476
473 477 .codeblock-label {
474 478 margin: 0 0 1em 0;
475 479 }
476 480 }
477 481
478 482 .code-body {
479 483 padding: 0.8em 1em;
480 484 background-color: #ffffff;
481 485 min-width: 100%;
482 486 box-sizing: border-box;
483 487 // TODO: johbo: Parent has overflow: auto, this forces the child here
484 488 // to have the intended size and to scroll. Should be simplified.
485 489 width: 100%;
486 490 overflow-x: auto;
487 491
488 492 img.rendered-binary {
489 493 height: auto;
490 494 width: 100%;
491 495 }
492 496
493 497 .markdown-block {
494 498 padding: 1em 0;
495 499 }
496 500 }
497 501
498 502 .codeblock-header {
499 503 background: @grey7;
500 504 height: 36px;
501 505 }
502 506
503 507 .path {
504 508 border-bottom: 1px solid @grey6;
505 509 padding: .65em 1em;
506 510 height: 18px;
507 511 }
508 512 }
509 513
510 514 .code-highlighttable,
511 515 div.codeblock {
512 516
513 517 &.readme {
514 518 background-color: white;
515 519 }
516 520
517 521 .markdown-block table {
518 522 border-collapse: collapse;
519 523
520 524 th,
521 525 td {
522 526 padding: .5em;
523 527 border: @border-thickness solid @border-default-color;
524 528 }
525 529 }
526 530
527 531 table {
528 532 border: 0px;
529 533 margin: 0;
530 534 letter-spacing: normal;
531 535
532 536
533 537 td {
534 538 border: 0px;
535 539 vertical-align: top;
536 540 }
537 541 }
538 542 }
539 543
540 544 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
541 545 div.search-code-body {
542 546 background-color: #ffffff; padding: 5px 0 5px 10px;
543 547 pre {
544 548 .match { background-color: #faffa6;}
545 549 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
546 550 }
547 551 .code-highlighttable {
548 552 border-collapse: collapse;
549 553
550 554 tr:hover {
551 555 background: #fafafa;
552 556 }
553 557 td.code {
554 558 padding-left: 10px;
555 559 }
556 560 td.line {
557 561 border-right: 1px solid #ccc !important;
558 562 padding-right: 10px;
559 563 text-align: right;
560 564 font-family: @text-monospace;
561 565 span {
562 566 white-space: pre-wrap;
563 567 color: #666666;
564 568 }
565 569 }
566 570 }
567 571 }
568 572
569 573 div.annotatediv { margin-left: 2px; margin-right: 4px; }
570 574 .code-highlight {
571 575 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
572 576 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
573 577 pre div:target {background-color: @comment-highlight-color !important;}
574 578 }
575 579
576 580 .linenos a { text-decoration: none; }
577 581
578 582 .CodeMirror-selected { background: @rchighlightblue; }
579 583 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
580 584 .CodeMirror ::selection { background: @rchighlightblue; }
581 585 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
582 586
583 587 .code { display: block; border:0px !important; }
584 588 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
585 589 /* This can be generated with `pygmentize -S default -f html` */
586 590 .codehilite {
587 591 .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
588 592 .hll { background-color: #ffffcc }
589 593 .c { color: #408080; font-style: italic } /* Comment */
590 594 .err, .codehilite .err { border: none } /* Error */
591 595 .k { color: #008000; font-weight: bold } /* Keyword */
592 596 .o { color: #666666 } /* Operator */
593 597 .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
594 598 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
595 599 .cp { color: #BC7A00 } /* Comment.Preproc */
596 600 .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
597 601 .c1 { color: #408080; font-style: italic } /* Comment.Single */
598 602 .cs { color: #408080; font-style: italic } /* Comment.Special */
599 603 .gd { color: #A00000 } /* Generic.Deleted */
600 604 .ge { font-style: italic } /* Generic.Emph */
601 605 .gr { color: #FF0000 } /* Generic.Error */
602 606 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
603 607 .gi { color: #00A000 } /* Generic.Inserted */
604 608 .go { color: #888888 } /* Generic.Output */
605 609 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
606 610 .gs { font-weight: bold } /* Generic.Strong */
607 611 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
608 612 .gt { color: #0044DD } /* Generic.Traceback */
609 613 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
610 614 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
611 615 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
612 616 .kp { color: #008000 } /* Keyword.Pseudo */
613 617 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
614 618 .kt { color: #B00040 } /* Keyword.Type */
615 619 .m { color: #666666 } /* Literal.Number */
616 620 .s { color: #BA2121 } /* Literal.String */
617 621 .na { color: #7D9029 } /* Name.Attribute */
618 622 .nb { color: #008000 } /* Name.Builtin */
619 623 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
620 624 .no { color: #880000 } /* Name.Constant */
621 625 .nd { color: #AA22FF } /* Name.Decorator */
622 626 .ni { color: #999999; font-weight: bold } /* Name.Entity */
623 627 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
624 628 .nf { color: #0000FF } /* Name.Function */
625 629 .nl { color: #A0A000 } /* Name.Label */
626 630 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
627 631 .nt { color: #008000; font-weight: bold } /* Name.Tag */
628 632 .nv { color: #19177C } /* Name.Variable */
629 633 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
630 634 .w { color: #bbbbbb } /* Text.Whitespace */
631 635 .mb { color: #666666 } /* Literal.Number.Bin */
632 636 .mf { color: #666666 } /* Literal.Number.Float */
633 637 .mh { color: #666666 } /* Literal.Number.Hex */
634 638 .mi { color: #666666 } /* Literal.Number.Integer */
635 639 .mo { color: #666666 } /* Literal.Number.Oct */
636 640 .sa { color: #BA2121 } /* Literal.String.Affix */
637 641 .sb { color: #BA2121 } /* Literal.String.Backtick */
638 642 .sc { color: #BA2121 } /* Literal.String.Char */
639 643 .dl { color: #BA2121 } /* Literal.String.Delimiter */
640 644 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
641 645 .s2 { color: #BA2121 } /* Literal.String.Double */
642 646 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
643 647 .sh { color: #BA2121 } /* Literal.String.Heredoc */
644 648 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
645 649 .sx { color: #008000 } /* Literal.String.Other */
646 650 .sr { color: #BB6688 } /* Literal.String.Regex */
647 651 .s1 { color: #BA2121 } /* Literal.String.Single */
648 652 .ss { color: #19177C } /* Literal.String.Symbol */
649 653 .bp { color: #008000 } /* Name.Builtin.Pseudo */
650 654 .fm { color: #0000FF } /* Name.Function.Magic */
651 655 .vc { color: #19177C } /* Name.Variable.Class */
652 656 .vg { color: #19177C } /* Name.Variable.Global */
653 657 .vi { color: #19177C } /* Name.Variable.Instance */
654 658 .vm { color: #19177C } /* Name.Variable.Magic */
655 659 .il { color: #666666 } /* Literal.Number.Integer.Long */
656 660
657 661 }
658 662
659 663 /* customized pre blocks for markdown/rst */
660 664 pre.literal-block, .codehilite pre{
661 665 padding: @padding;
662 666 border: 1px solid @grey6;
663 667 .border-radius(@border-radius);
664 668 background-color: @grey7;
665 669 }
666 670
667 671
668 672 /* START NEW CODE BLOCK CSS */
669 673
670 674 @cb-line-height: 18px;
671 675 @cb-line-code-padding: 10px;
672 676 @cb-text-padding: 5px;
673 677
674 678 @pill-padding: 2px 7px;
675 679 @pill-padding-small: 2px 2px 1px 2px;
676 680
677 681 input.filediff-collapse-state {
678 682 display: none;
679 683
680 684 &:checked + .filediff { /* file diff is collapsed */
681 685 .cb {
682 686 display: none
683 687 }
684 688 .filediff-collapse-indicator {
685 689 float: left;
686 690 cursor: pointer;
687 691 margin: 1px -5px;
688 692 }
689 693 .filediff-collapse-indicator:before {
690 694 content: '\f105';
691 695 }
692 696
693 697 .filediff-menu {
694 698 display: none;
695 699 }
696 700
697 701 }
698 702
699 703 &+ .filediff { /* file diff is expanded */
700 704
701 705 .filediff-collapse-indicator {
702 706 float: left;
703 707 cursor: pointer;
704 708 margin: 1px -5px;
705 709 }
706 710 .filediff-collapse-indicator:before {
707 711 content: '\f107';
708 712 }
709 713
710 714 .filediff-menu {
711 715 display: block;
712 716 }
713 717
714 718 margin: 10px 0;
715 719 &:nth-child(2) {
716 720 margin: 0;
717 721 }
718 722 }
719 723 }
720 724
721 725 .filediffs .anchor {
722 726 display: block;
723 727 height: 40px;
724 728 margin-top: -40px;
725 729 visibility: hidden;
726 730 }
727 731
728 732 .filediffs .anchor:nth-of-type(1) {
729 733 display: block;
730 734 height: 80px;
731 735 margin-top: -80px;
732 736 visibility: hidden;
733 737 }
734 738
735 739 .cs_files {
736 740 clear: both;
737 741 }
738 742
739 743 #diff-file-sticky{
740 744 will-change: min-height;
741 745 height: 80px;
742 746 }
743 747
744 748 .sidebar__inner{
745 749 transform: translate(0, 0); /* For browsers don't support translate3d. */
746 750 transform: translate3d(0, 0, 0);
747 751 will-change: position, transform;
748 752 height: 65px;
749 753 background-color: #fff;
750 754 padding: 5px 0px;
751 755 }
752 756
753 757 .sidebar__bar {
754 758 padding: 5px 0px 0px 0px
755 759 }
756 760
757 761 .fpath-placeholder {
758 762 clear: both;
759 763 visibility: hidden
760 764 }
761 765
762 766 .is-affixed {
763 767
764 768 .sidebar__inner {
765 769 z-index: 30;
766 770 }
767 771
768 772 .sidebar_inner_shadow {
769 773 position: fixed;
770 774 top: 75px;
771 775 right: -100%;
772 776 left: -100%;
773 777 z-index: 30;
774 778 display: block;
775 779 height: 5px;
776 780 content: "";
777 781 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
778 782 border-top: 1px solid rgba(0, 0, 0, 0.15);
779 783 }
780 784
781 785 .fpath-placeholder {
782 786 visibility: visible !important;
783 787 }
784 788 }
785 789
786 790 .diffset-menu {
787 791
788 792 }
789 793
790 794 #todo-box {
791 795 clear:both;
792 796 display: none;
793 797 text-align: right
794 798 }
795 799
796 800 .diffset {
797 801 margin: 0px auto;
798 802 .diffset-heading {
799 803 border: 1px solid @grey5;
800 804 margin-bottom: -1px;
801 805 // margin-top: 20px;
802 806 h2 {
803 807 margin: 0;
804 808 line-height: 38px;
805 809 padding-left: 10px;
806 810 }
807 811 .btn {
808 812 margin: 0;
809 813 }
810 814 background: @grey6;
811 815 display: block;
812 816 padding: 5px;
813 817 }
814 818 .diffset-heading-warning {
815 819 background: @alert3-inner;
816 820 border: 1px solid @alert3;
817 821 }
818 822 &.diffset-comments-disabled {
819 823 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
820 824 display: none !important;
821 825 }
822 826 }
823 827 }
824 828
825 829 .filelist {
826 830 .pill {
827 831 display: block;
828 832 float: left;
829 833 padding: @pill-padding-small;
830 834 }
831 835 }
832 836
833 837 .pill {
834 838 display: block;
835 839 float: left;
836 840 padding: @pill-padding;
837 841 }
838 842
839 843 .pill-group {
840 844 .pill {
841 845 opacity: .8;
842 846 margin-right: 3px;
843 847 font-size: 12px;
844 848 font-weight: normal;
845 849 min-width: 30px;
846 850 text-align: center;
847 851
848 852 &:first-child {
849 853 border-radius: @border-radius 0 0 @border-radius;
850 854 }
851 855 &:last-child {
852 856 border-radius: 0 @border-radius @border-radius 0;
853 857 }
854 858 &:only-child {
855 859 border-radius: @border-radius;
856 860 margin-right: 0;
857 861 }
858 862 }
859 863 }
860 864
861 865 /* Main comments*/
862 866 #comments {
863 867 .comment-selected {
864 868 border-left: 6px solid @comment-highlight-color;
865 869 padding-left: 3px;
866 870 margin-left: -9px;
867 871 }
868 872 }
869 873
870 874 .filediff {
871 875 border: 1px solid @grey5;
872 876
873 877 /* START OVERRIDES */
874 878 .code-highlight {
875 879 border: none; // TODO: remove this border from the global
876 880 // .code-highlight, it doesn't belong there
877 881 }
878 882 label {
879 883 margin: 0; // TODO: remove this margin definition from global label
880 884 // it doesn't belong there - if margin on labels
881 885 // are needed for a form they should be defined
882 886 // in the form's class
883 887 }
884 888 /* END OVERRIDES */
885 889
886 890 * {
887 891 box-sizing: border-box;
888 892 }
889 893 .filediff-anchor {
890 894 visibility: hidden;
891 895 }
892 896 &:hover {
893 897 .filediff-anchor {
894 898 visibility: visible;
895 899 }
896 900 }
897 901
898 902 .filediff-heading {
899 903 cursor: pointer;
900 904 display: block;
901 905 padding: 10px 10px;
902 906 }
903 907 .filediff-heading:after {
904 908 content: "";
905 909 display: table;
906 910 clear: both;
907 911 }
908 912 .filediff-heading:hover {
909 913 background: #e1e9f4 !important;
910 914 }
911 915
912 916 .filediff-menu {
913 917 text-align: right;
914 918 padding: 5px 5px 5px 0px;
915 919 background: @grey7;
916 920
917 921 &> a,
918 922 &> span {
919 923 padding: 1px;
920 924 }
921 925 }
922 926
923 927 .filediff-collapse-button, .filediff-expand-button {
924 928 cursor: pointer;
925 929 }
926 930 .filediff-collapse-button {
927 931 display: inline;
928 932 }
929 933 .filediff-expand-button {
930 934 display: none;
931 935 }
932 936 .filediff-collapsed .filediff-collapse-button {
933 937 display: none;
934 938 }
935 939 .filediff-collapsed .filediff-expand-button {
936 940 display: inline;
937 941 }
938 942
939 943 /**** COMMENTS ****/
940 944
941 945 .filediff-menu {
942 946 .show-comment-button {
943 947 display: none;
944 948 }
945 949 }
946 950 &.hide-comments {
947 951 .inline-comments {
948 952 display: none;
949 953 }
950 954 .filediff-menu {
951 955 .show-comment-button {
952 956 display: inline;
953 957 }
954 958 .hide-comment-button {
955 959 display: none;
956 960 }
957 961 }
958 962 }
959 963
960 964 .hide-line-comments {
961 965 .inline-comments {
962 966 display: none;
963 967 }
964 968 }
965 969
966 970 /**** END COMMENTS ****/
967 971
968 972 }
969 973
970 974
971 975 .op-added {
972 976 color: @alert1;
973 977 }
974 978
975 979 .op-deleted {
976 980 color: @alert2;
977 981 }
978 982
979 983 .filediff, .filelist {
980 984
981 985 .pill {
982 986 &[op="name"] {
983 987 background: none;
984 988 opacity: 1;
985 989 color: white;
986 990 }
987 991 &[op="limited"] {
988 992 background: @grey2;
989 993 color: white;
990 994 }
991 995 &[op="binary"] {
992 996 background: @color7;
993 997 color: white;
994 998 }
995 999 &[op="modified"] {
996 1000 background: @alert1;
997 1001 color: white;
998 1002 }
999 1003 &[op="renamed"] {
1000 1004 background: @color4;
1001 1005 color: white;
1002 1006 }
1003 1007 &[op="copied"] {
1004 1008 background: @color4;
1005 1009 color: white;
1006 1010 }
1007 1011 &[op="mode"] {
1008 1012 background: @grey3;
1009 1013 color: white;
1010 1014 }
1011 1015 &[op="symlink"] {
1012 1016 background: @color8;
1013 1017 color: white;
1014 1018 }
1015 1019
1016 1020 &[op="added"] { /* added lines */
1017 1021 background: @alert1;
1018 1022 color: white;
1019 1023 }
1020 1024 &[op="deleted"] { /* deleted lines */
1021 1025 background: @alert2;
1022 1026 color: white;
1023 1027 }
1024 1028
1025 1029 &[op="created"] { /* created file */
1026 1030 background: @alert1;
1027 1031 color: white;
1028 1032 }
1029 1033 &[op="removed"] { /* deleted file */
1030 1034 background: @color5;
1031 1035 color: white;
1032 1036 }
1033 1037 }
1034 1038 }
1035 1039
1036 1040
1037 1041 .filediff-outdated {
1038 1042 padding: 8px 0;
1039 1043
1040 1044 .filediff-heading {
1041 1045 opacity: .5;
1042 1046 }
1043 1047 }
1044 1048
1045 1049 table.cb {
1046 1050 width: 100%;
1047 1051 border-collapse: collapse;
1048 1052
1049 1053 .cb-text {
1050 1054 padding: @cb-text-padding;
1051 1055 }
1052 1056 .cb-hunk {
1053 1057 padding: @cb-text-padding;
1054 1058 }
1055 1059 .cb-expand {
1056 1060 display: none;
1057 1061 }
1058 1062 .cb-collapse {
1059 1063 display: inline;
1060 1064 }
1061 1065 &.cb-collapsed {
1062 1066 .cb-line {
1063 1067 display: none;
1064 1068 }
1065 1069 .cb-expand {
1066 1070 display: inline;
1067 1071 }
1068 1072 .cb-collapse {
1069 1073 display: none;
1070 1074 }
1071 1075 .cb-hunk {
1072 1076 display: none;
1073 1077 }
1074 1078 }
1075 1079
1076 1080 /* intentionally general selector since .cb-line-selected must override it
1077 1081 and they both use !important since the td itself may have a random color
1078 1082 generated by annotation blocks. TLDR: if you change it, make sure
1079 1083 annotated block selection and line selection in file view still work */
1080 1084 .cb-line-fresh .cb-content {
1081 1085 background: white !important;
1082 1086 }
1083 1087 .cb-warning {
1084 1088 background: #fff4dd;
1085 1089 }
1086 1090
1087 1091 &.cb-diff-sideside {
1088 1092 td {
1089 1093 &.cb-content {
1090 1094 width: 50%;
1091 1095 }
1092 1096 }
1093 1097 }
1094 1098
1095 1099 tr {
1096 1100 &.cb-annotate {
1097 1101 border-top: 1px solid #eee;
1098 1102 }
1099 1103
1100 1104 &.cb-comment-info {
1101 1105 border-top: 1px solid #eee;
1102 1106 color: rgba(0, 0, 0, 0.3);
1103 1107 background: #edf2f9;
1104 1108
1105 1109 td {
1106 1110
1107 1111 }
1108 1112 }
1109 1113
1110 1114 &.cb-hunk {
1111 1115 font-family: @text-monospace;
1112 1116 color: rgba(0, 0, 0, 0.3);
1113 1117
1114 1118 td {
1115 1119 &:first-child {
1116 1120 background: #edf2f9;
1117 1121 }
1118 1122 &:last-child {
1119 1123 background: #f4f7fb;
1120 1124 }
1121 1125 }
1122 1126 }
1123 1127 }
1124 1128
1125 1129
1126 1130 td {
1127 1131 vertical-align: top;
1128 1132 padding: 0;
1129 1133
1130 1134 &.cb-content {
1131 1135 font-size: 12.35px;
1132 1136
1133 1137 &.cb-line-selected .cb-code {
1134 1138 background: @comment-highlight-color !important;
1135 1139 }
1136 1140
1137 1141 span.cb-code {
1138 1142 line-height: @cb-line-height;
1139 1143 padding-left: @cb-line-code-padding;
1140 1144 padding-right: @cb-line-code-padding;
1141 1145 display: block;
1142 1146 white-space: pre-wrap;
1143 1147 font-family: @text-monospace;
1144 1148 word-break: break-all;
1145 1149 .nonl {
1146 1150 color: @color5;
1147 1151 }
1148 1152 .cb-action {
1149 1153 &:before {
1150 1154 content: " ";
1151 1155 }
1152 1156 &.cb-deletion:before {
1153 1157 content: "- ";
1154 1158 }
1155 1159 &.cb-addition:before {
1156 1160 content: "+ ";
1157 1161 }
1158 1162 }
1159 1163 }
1160 1164
1161 1165 &> button.cb-comment-box-opener {
1162 1166
1163 1167 padding: 2px 2px 1px 3px;
1164 1168 margin-left: -6px;
1165 1169 margin-top: -1px;
1166 1170
1167 1171 border-radius: @border-radius;
1168 1172 position: absolute;
1169 1173 display: none;
1170 1174 }
1171 1175 .cb-comment {
1172 1176 margin-top: 10px;
1173 1177 white-space: normal;
1174 1178 }
1175 1179 }
1176 1180 &:hover {
1177 1181 button.cb-comment-box-opener {
1178 1182 display: block;
1179 1183 }
1180 1184 &+ td button.cb-comment-box-opener {
1181 1185 display: block
1182 1186 }
1183 1187 }
1184 1188
1185 1189 &.cb-data {
1186 1190 text-align: right;
1187 1191 width: 30px;
1188 1192 font-family: @text-monospace;
1189 1193
1190 1194 .icon-comment {
1191 1195 cursor: pointer;
1192 1196 }
1193 1197 &.cb-line-selected {
1194 1198 background: @comment-highlight-color !important;
1195 1199 }
1196 1200 &.cb-line-selected > div {
1197 1201 display: block;
1198 1202 background: @comment-highlight-color !important;
1199 1203 line-height: @cb-line-height;
1200 1204 color: rgba(0, 0, 0, 0.3);
1201 1205 }
1202 1206 }
1203 1207
1204 1208 &.cb-lineno {
1205 1209 padding: 0;
1206 1210 width: 50px;
1207 1211 color: rgba(0, 0, 0, 0.3);
1208 1212 text-align: right;
1209 1213 border-right: 1px solid #eee;
1210 1214 font-family: @text-monospace;
1211 1215 -webkit-user-select: none;
1212 1216 -moz-user-select: none;
1213 1217 user-select: none;
1214 1218
1215 1219 a::before {
1216 1220 content: attr(data-line-no);
1217 1221 }
1218 1222 &.cb-line-selected {
1219 1223 background: @comment-highlight-color !important;
1220 1224 }
1221 1225
1222 1226 a {
1223 1227 display: block;
1224 1228 padding-right: @cb-line-code-padding;
1225 1229 padding-left: @cb-line-code-padding;
1226 1230 line-height: @cb-line-height;
1227 1231 color: rgba(0, 0, 0, 0.3);
1228 1232 }
1229 1233 }
1230 1234
1231 1235 &.cb-empty {
1232 1236 background: @grey7;
1233 1237 }
1234 1238
1235 1239 ins {
1236 1240 color: black;
1237 1241 background: #a6f3a6;
1238 1242 text-decoration: none;
1239 1243 }
1240 1244 del {
1241 1245 color: black;
1242 1246 background: #f8cbcb;
1243 1247 text-decoration: none;
1244 1248 }
1245 1249 &.cb-addition {
1246 1250 background: #ecffec;
1247 1251
1248 1252 &.blob-lineno {
1249 1253 background: #ddffdd;
1250 1254 }
1251 1255 }
1252 1256 &.cb-deletion {
1253 1257 background: #ffecec;
1254 1258
1255 1259 &.blob-lineno {
1256 1260 background: #ffdddd;
1257 1261 }
1258 1262 }
1259 1263 &.cb-annotate-message-spacer {
1260 1264 width:8px;
1261 1265 padding: 1px 0px 0px 3px;
1262 1266 }
1263 1267 &.cb-annotate-info {
1264 1268 width: 320px;
1265 1269 min-width: 320px;
1266 1270 max-width: 320px;
1267 1271 padding: 5px 2px;
1268 1272 font-size: 13px;
1269 1273
1270 1274 .cb-annotate-message {
1271 1275 padding: 2px 0px 0px 0px;
1272 1276 white-space: pre-line;
1273 1277 overflow: hidden;
1274 1278 }
1275 1279 .rc-user {
1276 1280 float: none;
1277 1281 padding: 0 6px 0 17px;
1278 1282 min-width: unset;
1279 1283 min-height: unset;
1280 1284 }
1281 1285 }
1282 1286
1283 1287 &.cb-annotate-revision {
1284 1288 cursor: pointer;
1285 1289 text-align: right;
1286 1290 padding: 1px 3px 0px 3px;
1287 1291 }
1288 1292 }
1289 1293 }
@@ -1,669 +1,691 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
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 Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 61 window.location = pyroutes.url('repo_compare', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 container.show();
80 80 if (!container.hasClass('loaded')) {
81 81 $.ajax({url: url})
82 82 .complete(function (data) {
83 83 var responseJSON = data.responseJSON;
84 84 container.addClass('loaded');
85 85 container.html(responseJSON.size);
86 86 callback(responseJSON.code_stats)
87 87 })
88 88 .fail(function (data) {
89 89 console.log('failed to load repo stats');
90 90 });
91 91 }
92 92
93 93 };
94 94
95 95 var showRepoStats = function(target, data){
96 96 var container = $('#' + target);
97 97
98 98 if (container.hasClass('loaded')) {
99 99 return
100 100 }
101 101
102 102 var total = 0;
103 103 var no_data = true;
104 104 var tbl = document.createElement('table');
105 105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106 106
107 107 $.each(data, function(key, val){
108 108 total += val.count;
109 109 });
110 110
111 111 var sortedStats = [];
112 112 for (var obj in data){
113 113 sortedStats.push([obj, data[obj]])
114 114 }
115 115 var sortedData = sortedStats.sort(function (a, b) {
116 116 return b[1].count - a[1].count
117 117 });
118 118 var cnt = 0;
119 119 $.each(sortedData, function(idx, val){
120 120 cnt += 1;
121 121 no_data = false;
122 122
123 123 var tr = document.createElement('tr');
124 124
125 125 var key = val[0];
126 126 var obj = {"desc": val[1].desc, "count": val[1].count};
127 127
128 128 // meta language names
129 129 var td1 = document.createElement('td');
130 130 var trending_language_label = document.createElement('div');
131 131 trending_language_label.innerHTML = obj.desc;
132 132 td1.appendChild(trending_language_label);
133 133
134 134 // extensions
135 135 var td2 = document.createElement('td');
136 136 var extension = document.createElement('div');
137 137 extension.innerHTML = ".{0}".format(key)
138 138 td2.appendChild(extension);
139 139
140 140 // number of files
141 141 var td3 = document.createElement('td');
142 142 var file_count = document.createElement('div');
143 143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 144 var label = _ngettext('file', 'files', obj.count);
145 145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 146 td3.appendChild(file_count);
147 147
148 148 // percentage
149 149 var td4 = document.createElement('td');
150 150 td4.setAttribute("class", 'trending_language');
151 151
152 152 var percentage = document.createElement('div');
153 153 percentage.setAttribute('class', 'lang-bar');
154 154 percentage.innerHTML = "&nbsp;";
155 155 percentage.style.width = percentage_num + '%';
156 156 td4.appendChild(percentage);
157 157
158 158 tr.appendChild(td1);
159 159 tr.appendChild(td2);
160 160 tr.appendChild(td3);
161 161 tr.appendChild(td4);
162 162 tbl.appendChild(tr);
163 163
164 164 });
165 165
166 166 $(container).html(tbl);
167 167 $(container).addClass('loaded');
168 168
169 169 $('#code_stats_show_more').on('click', function (e) {
170 170 e.preventDefault();
171 171 $('.stats_hidden').each(function (idx) {
172 172 $(this).css("display", "");
173 173 });
174 174 $('#code_stats_show_more').hide();
175 175 });
176 176
177 177 };
178 178
179 179 // returns a node from given html;
180 180 var fromHTML = function(html){
181 181 var _html = document.createElement('element');
182 182 _html.innerHTML = html;
183 183 return _html;
184 184 };
185 185
186 186 // Toggle Collapsable Content
187 187 function collapsableContent() {
188 188
189 189 $('.collapsable-content').not('.no-hide').hide();
190 190
191 191 $('.btn-collapse').unbind(); //in case we've been here before
192 192 $('.btn-collapse').click(function() {
193 193 var button = $(this);
194 194 var togglename = $(this).data("toggle");
195 195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 196 if ($(this).html()=="Show Less")
197 197 $(this).html("Show More");
198 198 else
199 199 $(this).html("Show Less");
200 200 });
201 201 };
202 202
203 203 var timeagoActivate = function() {
204 204 $("time.timeago").timeago();
205 205 };
206 206
207 207
208 208 var clipboardActivate = function() {
209 209 /*
210 210 *
211 211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 212 * */
213 213 var clipboard = new ClipboardJS('.clipboard-action');
214 214
215 215 clipboard.on('success', function(e) {
216 216 var callback = function () {
217 217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 218 };
219 219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 220 e.clearSelection();
221 221 });
222 222 };
223 223
224 224 var tooltipActivate = function () {
225 225 var delay = 50;
226 226 var animation = 'fade';
227 227 var theme = 'tooltipster-shadow';
228 228 var debug = false;
229 229
230 230 $('.tooltip').tooltipster({
231 231 debug: debug,
232 232 theme: theme,
233 233 animation: animation,
234 234 delay: delay,
235 235 contentCloning: true,
236 236 contentAsHTML: true,
237 237
238 238 functionBefore: function (instance, helper) {
239 239 var $origin = $(helper.origin);
240 240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 241 instance.content(data);
242 242 }
243 243 });
244 244 var hovercardCache = {};
245 245
246 246 var loadHoverCard = function (url, altHovercard, callback) {
247 247 var id = url;
248 248
249 249 if (hovercardCache[id] !== undefined) {
250 250 callback(hovercardCache[id]);
251 251 return true;
252 252 }
253 253
254 254 hovercardCache[id] = undefined;
255 255 $.get(url, function (data) {
256 256 hovercardCache[id] = data;
257 257 callback(hovercardCache[id]);
258 258 return true;
259 259 }).fail(function (data, textStatus, errorThrown) {
260 260
261 261 if (parseInt(data.status) === 404) {
262 262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
263 263 } else {
264 264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
265 265 }
266 266 callback(msg);
267 267 return false
268 268 });
269 269 };
270 270
271 271 $('.tooltip-hovercard').tooltipster({
272 272 debug: debug,
273 273 theme: theme,
274 274 animation: animation,
275 275 delay: delay,
276 276 interactive: true,
277 277 contentCloning: true,
278 278
279 279 trigger: 'custom',
280 280 triggerOpen: {
281 281 mouseenter: true,
282 282 },
283 283 triggerClose: {
284 284 mouseleave: true,
285 285 originClick: true,
286 286 touchleave: true
287 287 },
288 288 content: _gettext('Loading...'),
289 289 contentAsHTML: true,
290 290 updateAnimation: null,
291 291
292 292 functionBefore: function (instance, helper) {
293 293
294 294 var $origin = $(helper.origin);
295 295
296 296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
297 297 if ($origin.data('loaded') !== true) {
298 298 var hovercardUrl = $origin.data('hovercardUrl');
299 299 var altHovercard =$origin.data('hovercardAlt');
300 300
301 301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
302 302 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
303 303 instance.content(data);
304 304 })
305 305 } else {
306 306 if ($origin.data('hovercardAltHtml')) {
307 307 var data = atob($origin.data('hovercardAltHtml'));
308 308 } else {
309 309 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
310 310 }
311 311 var loaded = true;
312 312 instance.content(data);
313 313 }
314 314
315 315 // to remember that the data has been loaded
316 316 $origin.data('loaded', loaded);
317 317 }
318 318 }
319 319 })
320 320 };
321 321
322 322 // Formatting values in a Select2 dropdown of commit references
323 323 var formatSelect2SelectionRefs = function(commit_ref){
324 324 var tmpl = '';
325 325 if (!commit_ref.text || commit_ref.type === 'sha'){
326 326 return commit_ref.text;
327 327 }
328 328 if (commit_ref.type === 'branch'){
329 329 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
330 330 } else if (commit_ref.type === 'tag'){
331 331 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
332 332 } else if (commit_ref.type === 'book'){
333 333 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
334 334 }
335 335 return tmpl.concat(escapeHtml(commit_ref.text));
336 336 };
337 337
338 338 // takes a given html element and scrolls it down offset pixels
339 339 function offsetScroll(element, offset) {
340 340 setTimeout(function() {
341 341 var location = element.offset().top;
342 342 // some browsers use body, some use html
343 343 $('html, body').animate({ scrollTop: (location - offset) });
344 344 }, 100);
345 345 }
346 346
347 347 // scroll an element `percent`% from the top of page in `time` ms
348 348 function scrollToElement(element, percent, time) {
349 349 percent = (percent === undefined ? 25 : percent);
350 350 time = (time === undefined ? 100 : time);
351 351
352 352 var $element = $(element);
353 353 if ($element.length == 0) {
354 354 throw('Cannot scroll to {0}'.format(element))
355 355 }
356 356 var elOffset = $element.offset().top;
357 357 var elHeight = $element.height();
358 358 var windowHeight = $(window).height();
359 359 var offset = elOffset;
360 360 if (elHeight < windowHeight) {
361 361 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
362 362 }
363 363 setTimeout(function() {
364 364 $('html, body').animate({ scrollTop: offset});
365 365 }, time);
366 366 }
367 367
368 368 /**
369 369 * global hooks after DOM is loaded
370 370 */
371 371 $(document).ready(function() {
372 372 firefoxAnchorFix();
373 373
374 374 $('.navigation a.menulink').on('click', function(e){
375 375 var menuitem = $(this).parent('li');
376 376 if (menuitem.hasClass('open')) {
377 377 menuitem.removeClass('open');
378 378 } else {
379 379 menuitem.addClass('open');
380 380 $(document).on('click', function(event) {
381 381 if (!$(event.target).closest(menuitem).length) {
382 382 menuitem.removeClass('open');
383 383 }
384 384 });
385 385 }
386 386 });
387 387
388 388 $('body').on('click', '.cb-lineno a', function(event) {
389 389 function sortNumber(a,b) {
390 390 return a - b;
391 391 }
392 392
393 393 var lineNo = $(this).data('lineNo');
394 394 var lineName = $(this).attr('name');
395 395
396 396 if (lineNo) {
397 397 var prevLine = $('.cb-line-selected a').data('lineNo');
398 398
399 399 // on shift, we do a range selection, if we got previous line
400 400 if (event.shiftKey && prevLine !== undefined) {
401 401 var prevLine = parseInt(prevLine);
402 402 var nextLine = parseInt(lineNo);
403 403 var pos = [prevLine, nextLine].sort(sortNumber);
404 404 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
405 405
406 406 // single click
407 407 } else {
408 408 var nextLine = parseInt(lineNo);
409 409 var pos = [nextLine, nextLine];
410 410 var anchor = '#L{0}'.format(pos[0]);
411 411
412 412 }
413 413 // highlight
414 414 var range = [];
415 415 for (var i = pos[0]; i <= pos[1]; i++) {
416 416 range.push(i);
417 417 }
418 418 // clear old selected lines
419 419 $('.cb-line-selected').removeClass('cb-line-selected');
420 420
421 421 $.each(range, function (i, lineNo) {
422 422 var line_td = $('td.cb-lineno#L' + lineNo);
423 423
424 424 if (line_td.length) {
425 425 line_td.addClass('cb-line-selected'); // line number td
426 426 line_td.prev().addClass('cb-line-selected'); // line data
427 427 line_td.next().addClass('cb-line-selected'); // line content
428 428 }
429 429 });
430 430
431 431 } else if (lineName !== undefined) { // lineName only occurs in diffs
432 432 // clear old selected lines
433 433 $('td.cb-line-selected').removeClass('cb-line-selected');
434 434 var anchor = '#{0}'.format(lineName);
435 435 var diffmode = templateContext.session_attrs.diffmode || "sideside";
436 436
437 437 if (diffmode === "unified") {
438 438 $(this).closest('tr').find('td').addClass('cb-line-selected');
439 439 } else {
440 440 var activeTd = $(this).closest('td');
441 441 activeTd.addClass('cb-line-selected');
442 442 activeTd.next('td').addClass('cb-line-selected');
443 443 }
444 444
445 445 }
446 446
447 447 // Replace URL without jumping to it if browser supports.
448 448 // Default otherwise
449 449 if (history.pushState && anchor !== undefined) {
450 450 var new_location = location.href.rstrip('#');
451 451 if (location.hash) {
452 452 // location without hash
453 453 new_location = new_location.replace(location.hash, "");
454 454 }
455 455
456 456 // Make new anchor url
457 457 new_location = new_location + anchor;
458 458 history.pushState(true, document.title, new_location);
459 459
460 460 return false;
461 461 }
462 462
463 463 });
464 464
465 465 $('.collapse_file').on('click', function(e) {
466 466 e.stopPropagation();
467 467 if ($(e.target).is('a')) { return; }
468 468 var node = $(e.delegateTarget).first();
469 469 var icon = $($(node.children().first()).children().first());
470 470 var id = node.attr('fid');
471 471 var target = $('#'+id);
472 472 var tr = $('#tr_'+id);
473 473 var diff = $('#diff_'+id);
474 474 if(node.hasClass('expand_file')){
475 475 node.removeClass('expand_file');
476 476 icon.removeClass('expand_file_icon');
477 477 node.addClass('collapse_file');
478 478 icon.addClass('collapse_file_icon');
479 479 diff.show();
480 480 tr.show();
481 481 target.show();
482 482 } else {
483 483 node.removeClass('collapse_file');
484 484 icon.removeClass('collapse_file_icon');
485 485 node.addClass('expand_file');
486 486 icon.addClass('expand_file_icon');
487 487 diff.hide();
488 488 tr.hide();
489 489 target.hide();
490 490 }
491 491 });
492 492
493 493 $('#expand_all_files').click(function() {
494 494 $('.expand_file').each(function() {
495 495 var node = $(this);
496 496 var icon = $($(node.children().first()).children().first());
497 497 var id = $(this).attr('fid');
498 498 var target = $('#'+id);
499 499 var tr = $('#tr_'+id);
500 500 var diff = $('#diff_'+id);
501 501 node.removeClass('expand_file');
502 502 icon.removeClass('expand_file_icon');
503 503 node.addClass('collapse_file');
504 504 icon.addClass('collapse_file_icon');
505 505 diff.show();
506 506 tr.show();
507 507 target.show();
508 508 });
509 509 });
510 510
511 511 $('#collapse_all_files').click(function() {
512 512 $('.collapse_file').each(function() {
513 513 var node = $(this);
514 514 var icon = $($(node.children().first()).children().first());
515 515 var id = $(this).attr('fid');
516 516 var target = $('#'+id);
517 517 var tr = $('#tr_'+id);
518 518 var diff = $('#diff_'+id);
519 519 node.removeClass('collapse_file');
520 520 icon.removeClass('collapse_file_icon');
521 521 node.addClass('expand_file');
522 522 icon.addClass('expand_file_icon');
523 523 diff.hide();
524 524 tr.hide();
525 525 target.hide();
526 526 });
527 527 });
528 528
529 529 // Mouse over behavior for comments and line selection
530 530
531 531 // Select the line that comes from the url anchor
532 532 // At the time of development, Chrome didn't seem to support jquery's :target
533 533 // element, so I had to scroll manually
534 534
535 535 if (location.hash) {
536 536 var result = splitDelimitedHash(location.hash);
537 var loc = result.loc;
537
538 var loc = result.loc;
539
538 540 if (loc.length > 1) {
539 541
540 542 var highlightable_line_tds = [];
541 543
542 544 // source code line format
543 var page_highlights = loc.substring(
544 loc.indexOf('#') + 1).split('L');
545 var page_highlights = loc.substring(loc.indexOf('#') + 1).split('L');
545 546
547 // multi-line HL, for files
546 548 if (page_highlights.length > 1) {
547 549 var highlight_ranges = page_highlights[1].split(",");
548 550 var h_lines = [];
549 551 for (var pos in highlight_ranges) {
550 552 var _range = highlight_ranges[pos].split('-');
551 553 if (_range.length === 2) {
552 554 var start = parseInt(_range[0]);
553 555 var end = parseInt(_range[1]);
554 556 if (start < end) {
555 557 for (var i = start; i <= end; i++) {
556 558 h_lines.push(i);
557 559 }
558 560 }
559 }
560 else {
561 } else {
561 562 h_lines.push(parseInt(highlight_ranges[pos]));
562 563 }
563 564 }
564 565 for (pos in h_lines) {
565 566 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
566 567 if (line_td.length) {
567 568 highlightable_line_tds.push(line_td);
568 569 }
569 570 }
570 571 }
571 572
572 // now check a direct id reference (diff page)
573 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
573 // now check a direct id reference of line in diff / pull-request page)
574 if ($(loc).length > 0 && $(loc).hasClass('cb-lineno')) {
574 575 highlightable_line_tds.push($(loc));
575 576 }
577
578 // mark diff lines as selected
576 579 $.each(highlightable_line_tds, function (i, $td) {
577 580 $td.addClass('cb-line-selected'); // line number td
578 581 $td.prev().addClass('cb-line-selected'); // line data
579 582 $td.next().addClass('cb-line-selected'); // line content
580 583 });
581 584
582 if (highlightable_line_tds.length) {
585 if (highlightable_line_tds.length > 0) {
583 586 var $first_line_td = highlightable_line_tds[0];
584 587 scrollToElement($first_line_td);
585 588 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
586 589 td: $first_line_td,
587 590 remainder: result.remainder
588 591 });
592 } else {
593 // case for direct anchor to comments
594 var $line = $(loc);
595
596 if ($line.hasClass('comment-general')) {
597 $line.show();
598 } else if ($line.hasClass('comment-inline')) {
599 $line.show();
600 var $cb = $line.closest('.cb');
601 $cb.removeClass('cb-collapsed')
602 }
603 if ($line.length > 0) {
604 $line.addClass('comment-selected-hl');
605 offsetScroll($line, 70);
606 }
607 if (!$line.hasClass('comment-outdated') && result.remainder === '/ReplyToComment') {
608 $line.nextAll('.cb-comment-add-button').trigger('click');
609 }
589 610 }
611
590 612 }
591 613 }
592 614 collapsableContent();
593 615 });
594 616
595 617 var feedLifetimeOptions = function(query, initialData){
596 618 var data = {results: []};
597 619 var isQuery = typeof query.term !== 'undefined';
598 620
599 621 var section = _gettext('Lifetime');
600 622 var children = [];
601 623
602 624 //filter results
603 625 $.each(initialData.results, function(idx, value) {
604 626
605 627 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
606 628 children.push({
607 629 'id': this.id,
608 630 'text': this.text
609 631 })
610 632 }
611 633
612 634 });
613 635 data.results.push({
614 636 'text': section,
615 637 'children': children
616 638 });
617 639
618 640 if (isQuery) {
619 641
620 642 var now = moment.utc();
621 643
622 644 var parseQuery = function(entry, now){
623 645 var fmt = 'DD/MM/YYYY H:mm';
624 646 var parsed = moment.utc(entry, fmt);
625 647 var diffInMin = parsed.diff(now, 'minutes');
626 648
627 649 if (diffInMin > 0){
628 650 return {
629 651 id: diffInMin,
630 652 text: parsed.format(fmt)
631 653 }
632 654 } else {
633 655 return {
634 656 id: undefined,
635 657 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
636 658 }
637 659 }
638 660
639 661
640 662 };
641 663
642 664 data.results.push({
643 665 'text': _gettext('Specified expiration date'),
644 666 'children': [{
645 667 'id': parseQuery(query.term, now).id,
646 668 'text': parseQuery(query.term, now).text
647 669 }]
648 670 });
649 671 }
650 672
651 673 query.callback(data);
652 674 };
653 675
654 676
655 677 var storeUserSessionAttr = function (key, val) {
656 678
657 679 var postData = {
658 680 'key': key,
659 681 'val': val,
660 682 'csrf_token': CSRF_TOKEN
661 683 };
662 684
663 685 var success = function(o) {
664 686 return true
665 687 };
666 688
667 689 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
668 690 return false;
669 691 };
@@ -1,314 +1,302 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
6 6 <%namespace name="file_base" file="/files/base.mako"/>
7 7
8 8 <%def name="title()">
9 9 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='repositories')}
17 17 </%def>
18 18
19 19 <%def name="menu_bar_subnav()">
20 20 ${self.repo_menu(active='commits')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 <script type="text/javascript">
25 25 // TODO: marcink switch this to pyroutes
26 26 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
27 27 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
28 28 </script>
29 29
30 30 <div class="box">
31 31
32 32 <div class="summary">
33 33
34 34 <div class="fieldset">
35 35 <div class="left-content">
36 36 <%
37 37 rc_user = h.discover_user(c.commit.author_email)
38 38 %>
39 39 <div class="left-content-avatar">
40 40 ${base.gravatar(c.commit.author_email, 30, tooltip=True, user=rc_user)}
41 41 </div>
42 42
43 43 <div class="left-content-message">
44 44 <div class="fieldset collapsable-content no-hide" data-toggle="summary-details">
45 45 <div class="commit truncate-wrap">${h.urlify_commit_message(h.chop_at_smart(c.commit.message, '\n', suffix_if_chopped='...'), c.repo_name)}</div>
46 46 </div>
47 47
48 48 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none">
49 49 <div class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
50 50 </div>
51 51
52 52 <div class="fieldset" data-toggle="summary-details">
53 53 <div class="">
54 54 <table>
55 55 <tr class="file_author">
56 56
57 57 <td>
58 58 <span class="user commit-author">${h.link_to_user(rc_user or c.commit.author)}</span>
59 59 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
60 60 </td>
61 61
62 62 <td>
63 63 ## second cell for consistency with files
64 64 </td>
65 65 </tr>
66 66 </table>
67 67 </div>
68 68 </div>
69 69
70 70 </div>
71 71 </div>
72 72
73 73 <div class="right-content">
74 74
75 75 <div data-toggle="summary-details">
76 76 <div class="tags tags-main">
77 77 <code><a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">${h.show_id(c.commit)}</a></code>
78 78 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
79 79 ${file_base.refs(c.commit)}
80 80
81 81 ## phase
82 82 % if hasattr(c.commit, 'phase') and getattr(c.commit, 'phase') != 'public':
83 83 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">
84 84 <i class="icon-info"></i>${c.commit.phase}
85 85 </span>
86 86 % endif
87 87
88 88 ## obsolete commits
89 89 % if getattr(c.commit, 'obsolete', False):
90 90 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">
91 91 ${_('obsolete')}
92 92 </span>
93 93 % endif
94 94
95 95 ## hidden commits
96 96 % if getattr(c.commit, 'hidden', False):
97 97 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">
98 98 ${_('hidden')}
99 99 </span>
100 100 % endif
101 101 </div>
102 102
103 103 %if c.statuses:
104 104 <div class="tag status-tag-${c.statuses[0]} pull-right">
105 105 <i class="icon-circle review-status-${c.statuses[0]}"></i>
106 106 <div class="pull-right">${h.commit_status_lbl(c.statuses[0])}</div>
107 107 </div>
108 108 %endif
109 109
110 110 </div>
111 111
112 112 </div>
113 113 </div>
114 114
115 115 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
116 116 <div class="left-label-summary">
117 117 <p>${_('Commit navigation')}:</p>
118 118 <div class="right-label-summary">
119 119 <span id="parent_link" class="tag tagtag">
120 120 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
121 121 </span>
122 122
123 123 <span id="child_link" class="tag tagtag">
124 124 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
125 125 </span>
126 126 </div>
127 127 </div>
128 128 </div>
129 129
130 130 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
131 131 <div class="left-label-summary">
132 132 <p>${_('Diff options')}:</p>
133 133 <div class="right-label-summary">
134 134 <div class="diff-actions">
135 135 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
136 136 ${_('Raw Diff')}
137 137 </a>
138 138 |
139 139 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
140 140 ${_('Patch Diff')}
141 141 </a>
142 142 |
143 143 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}">
144 144 ${_('Download Diff')}
145 145 </a>
146 146 </div>
147 147 </div>
148 148 </div>
149 149 </div>
150 150
151 151 <div class="clear-fix"></div>
152 152
153 153 <div class="btn-collapse" data-toggle="summary-details">
154 154 ${_('Show More')}
155 155 </div>
156 156
157 157 </div>
158 158
159 159 <div class="cs_files">
160 160 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
161 161 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id])}
162 162 ${cbdiffs.render_diffset(
163 163 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
164 164 </div>
165 165
166 166 ## template for inline comment form
167 167 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
168 168
169 169 ## comments heading with count
170 170 <div class="comments-heading">
171 171 <i class="icon-comment"></i>
172 172 ${_('Comments')} ${len(c.comments)}
173 173 </div>
174 174
175 175 ## render comments
176 176 ${comment.generate_comments(c.comments)}
177 177
178 178 ## main comment form and it status
179 179 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
180 180 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
181 181 </div>
182 182
183 183 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
184 184 <script type="text/javascript">
185 185
186 186 $(document).ready(function() {
187 187
188 188 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
189 189 if($('#trimmed_message_box').height() === boxmax){
190 190 $('#message_expand').show();
191 191 }
192 192
193 193 $('#message_expand').on('click', function(e){
194 194 $('#trimmed_message_box').css('max-height', 'none');
195 195 $(this).hide();
196 196 });
197 197
198 198 $('.show-inline-comments').on('click', function(e){
199 199 var boxid = $(this).attr('data-comment-id');
200 200 var button = $(this);
201 201
202 202 if(button.hasClass("comments-visible")) {
203 203 $('#{0} .inline-comments'.format(boxid)).each(function(index){
204 204 $(this).hide();
205 205 });
206 206 button.removeClass("comments-visible");
207 207 } else {
208 208 $('#{0} .inline-comments'.format(boxid)).each(function(index){
209 209 $(this).show();
210 210 });
211 211 button.addClass("comments-visible");
212 212 }
213 213 });
214 214
215
216 215 // next links
217 216 $('#child_link').on('click', function(e){
218 217 // fetch via ajax what is going to be the next link, if we have
219 218 // >1 links show them to user to choose
220 219 if(!$('#child_link').hasClass('disabled')){
221 220 $.ajax({
222 221 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
223 222 success: function(data) {
224 223 if(data.results.length === 0){
225 224 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
226 225 }
227 226 if(data.results.length === 1){
228 227 var commit = data.results[0];
229 228 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
230 229 }
231 230 else if(data.results.length === 2){
232 231 $('#child_link').addClass('disabled');
233 232 $('#child_link').addClass('double');
234 233
235 234 var _html = '';
236 235 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
237 236 .replace('__branch__', data.results[0].branch)
238 237 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
239 238 .replace('__title__', data.results[0].message)
240 239 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
241 240 _html +=' | ';
242 241 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
243 242 .replace('__branch__', data.results[1].branch)
244 243 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
245 244 .replace('__title__', data.results[1].message)
246 245 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
247 246 $('#child_link').html(_html);
248 247 }
249 248 }
250 249 });
251 250 e.preventDefault();
252 251 }
253 252 });
254 253
255 254 // prev links
256 255 $('#parent_link').on('click', function(e){
257 256 // fetch via ajax what is going to be the next link, if we have
258 257 // >1 links show them to user to choose
259 258 if(!$('#parent_link').hasClass('disabled')){
260 259 $.ajax({
261 260 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
262 261 success: function(data) {
263 262 if(data.results.length === 0){
264 263 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
265 264 }
266 265 if(data.results.length === 1){
267 266 var commit = data.results[0];
268 267 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
269 268 }
270 269 else if(data.results.length === 2){
271 270 $('#parent_link').addClass('disabled');
272 271 $('#parent_link').addClass('double');
273 272
274 273 var _html = '';
275 274 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
276 275 .replace('__branch__', data.results[0].branch)
277 276 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
278 277 .replace('__title__', data.results[0].message)
279 278 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
280 279 _html +=' | ';
281 280 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
282 281 .replace('__branch__', data.results[1].branch)
283 282 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
284 283 .replace('__title__', data.results[1].message)
285 284 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
286 285 $('#parent_link').html(_html);
287 286 }
288 287 }
289 288 });
290 289 e.preventDefault();
291 290 }
292 291 });
293 292
294 if (location.hash) {
295 var result = splitDelimitedHash(location.hash);
296 var line = $('html').find(result.loc);
297 if (line.length > 0){
298 offsetScroll(line, 70);
299 }
300 }
301
302 293 // browse tree @ revision
303 294 $('#files_link').on('click', function(e){
304 295 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
305 296 e.preventDefault();
306 297 });
307 298
308 // inject comments into their proper positions
309 var file_comments = $('.inline-comment-placeholder');
310
311 299 })
312 300 </script>
313 301
314 302 </%def>
@@ -1,161 +1,170 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': '@'+h.person(user),
10 10 'repo_name': repo_name,
11 11 'status': status_change,
12 12 'comment_file': comment_file,
13 13 'comment_line': comment_line,
14 14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 16
16 17 'commit_id': h.show_id(commit),
17 18 }
18 19 %>
19 20
20 21
21 22 % if comment_file:
22 23 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
23 24 % else:
24 25 % if status_change:
25 26 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
26 27 % else:
27 28 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
28 29 % endif
29 30 % endif
30 31
31 32 </%def>
32 33
33 34 ## PLAINTEXT VERSION OF BODY
34 35 <%def name="body_plaintext()" filter="n,trim">
35 36 <%
36 37 data = {
37 38 'user': h.person(user),
38 39 'repo_name': repo_name,
39 40 'status': status_change,
40 41 'comment_file': comment_file,
41 42 'comment_line': comment_line,
42 43 'comment_type': comment_type,
44 'comment_id': comment_id,
43 45
44 46 'commit_id': h.show_id(commit),
45 47 }
46 48 %>
47 49
48 50 * ${_('Comment link')}: ${commit_comment_url}
49 51
50 52 %if status_change:
51 53 * ${_('Commit status')}: ${_('Status was changed to')}: *${status_change}*
52 54
53 55 %endif
54 56 * ${_('Commit')}: ${h.show_id(commit)}
55 57
56 58 * ${_('Commit message')}: ${commit.message}
57 59
58 60 %if comment_file:
59 61 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
60 62
61 63 %endif
62 64 % if comment_type == 'todo':
63 65 ${_('`TODO` comment')}:
64 66 % else:
65 67 ${_('`Note` comment')}:
66 68 % endif
67 69
68 70 ${comment_body |n, trim}
69 71
70 72 ---
71 73 ${self.plaintext_footer()}
72 74 </%def>
73 75
74 76
75 77 <%
76 78 data = {
77 79 'user': h.person(user),
78 80 'comment_file': comment_file,
79 81 'comment_line': comment_line,
80 82 'comment_type': comment_type,
83 'comment_id': comment_id,
81 84 'renderer_type': renderer_type or 'plain',
82 85
83 86 'repo': commit_target_repo_url,
84 87 'repo_name': repo_name,
85 88 'commit_id': h.show_id(commit),
86 89 }
87 90 %>
88 91
89 92 <table style="text-align:left;vertical-align:middle;width: 100%">
90 93 <tr>
91 94 <td style="width:100%;border-bottom:1px solid #dbd9da;">
92 95
93 96 <h4 style="margin: 0">
94 97 <div style="margin-bottom: 4px; color:#7E7F7F">
95 98 @${h.person(user.username)}
96 99 </div>
97 100 ${_('left a')}
98 101 <a href="${commit_comment_url}" style="${base.link_css()}">
99 102 % if comment_file:
100 103 ${_('{comment_type} on file `{comment_file}` in commit.').format(**data)}
101 104 % else:
102 105 ${_('{comment_type} on commit.').format(**data) |n}
103 106 % endif
104 107 </a>
105 108 <div style="margin-top: 10px"></div>
106 109 ${_('Commit')} <code>${data['commit_id']}</code> ${_('of repository')}: ${data['repo_name']}
107 110 </h4>
108 111
109 112 </td>
110 113 </tr>
111 114
112 115 </table>
113 116
114 117 <table style="text-align:left;vertical-align:middle;width: 100%">
115 118
116 119 ## spacing def
117 120 <tr>
118 121 <td style="width: 130px"></td>
119 122 <td></td>
120 123 </tr>
121 124
122 125 % if status_change:
123 126 <tr>
124 127 <td style="padding-right:20px;">${_('Commit Status')}:</td>
125 128 <td>
126 129 ${_('Status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
127 130 </td>
128 131 </tr>
129 132 % endif
130 133
131 134 <tr>
132 135 <td style="padding-right:20px;">${_('Commit')}:</td>
133 136 <td>
134 137 <a href="${commit_comment_url}" style="${base.link_css()}">${h.show_id(commit)}</a>
135 138 </td>
136 139 </tr>
137 140 <tr>
138 141 <td style="padding-right:20px;">${_('Commit message')}:</td>
139 142 <td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td>
140 143 </tr>
141 144
142 145 % if comment_file:
143 146 <tr>
144 147 <td style="padding-right:20px;">${_('File')}:</td>
145 148 <td><a href="${commit_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
146 149 </tr>
147 150 % endif
148 151
149 <tr style="background-image: linear-gradient(to right, black 33%, rgba(255,255,255,0) 0%);background-position: bottom;background-size: 3px 1px;background-repeat: repeat-x;">
152 <tr style="border-bottom:1px solid #dbd9da;">
150 153 <td colspan="2" style="padding-right:20px;">
151 154 % if comment_type == 'todo':
152 ${_('`TODO` comment')}:
155 ${_('`TODO` number')} ${comment_id}:
153 156 % else:
154 ${_('`Note` comment')}:
157 ${_('`Note` number')} ${comment_id}:
155 158 % endif
156 159 </td>
157 160 </tr>
158 161
159 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
162 <tr>
163 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
164 </tr>
165
166 <tr>
167 <td><a href="${commit_comment_reply_url}">${_('Reply')}</a></td>
168 <td></td>
160 169 </tr>
161 170 </table>
@@ -1,191 +1,200 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': '@'+h.person(user),
10 10 'repo_name': repo_name,
11 11 'status': status_change,
12 12 'comment_file': comment_file,
13 13 'comment_line': comment_line,
14 14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 16
16 17 'pr_title': pull_request.title,
17 18 'pr_id': pull_request.pull_request_id,
18 19 }
19 20 %>
20 21
21 22
22 23 % if comment_file:
23 24 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
24 25 % else:
25 26 % if status_change:
26 27 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
27 28 % else:
28 29 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
29 30 % endif
30 31 % endif
31 32
32 33 </%def>
33 34
34 35 ## PLAINTEXT VERSION OF BODY
35 36 <%def name="body_plaintext()" filter="n,trim">
36 37 <%
37 38 data = {
38 39 'user': h.person(user),
39 40 'repo_name': repo_name,
40 41 'status': status_change,
41 42 'comment_file': comment_file,
42 43 'comment_line': comment_line,
43 44 'comment_type': comment_type,
45 'comment_id': comment_id,
44 46
45 47 'pr_title': pull_request.title,
46 48 'pr_id': pull_request.pull_request_id,
47 49 'source_ref_type': pull_request.source_ref_parts.type,
48 50 'source_ref_name': pull_request.source_ref_parts.name,
49 51 'target_ref_type': pull_request.target_ref_parts.type,
50 52 'target_ref_name': pull_request.target_ref_parts.name,
51 53 'source_repo': pull_request_source_repo.repo_name,
52 54 'target_repo': pull_request_target_repo.repo_name,
53 55 'source_repo_url': pull_request_source_repo_url,
54 56 'target_repo_url': pull_request_target_repo_url,
55 57 }
56 58 %>
57 59
58 60 ${h.literal(_('Pull request !{pr_id}: `{pr_title}`').format(**data))}
59 61
60 62 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
61 63
62 64 * ${_('Comment link')}: ${pr_comment_url}
63 65
64 66 %if status_change and not closing_pr:
65 67 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
66 68
67 69 %elif status_change and closing_pr:
68 70 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
69 71
70 72 %endif
71 73 %if comment_file:
72 74 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
73 75
74 76 %endif
75 77 % if comment_type == 'todo':
76 78 ${_('`TODO` comment')}:
77 79 % else:
78 80 ${_('`Note` comment')}:
79 81 % endif
80 82
81 83 ${comment_body |n, trim}
82 84
83 85 ---
84 86 ${self.plaintext_footer()}
85 87 </%def>
86 88
87 89
88 90 <%
89 91 data = {
90 92 'user': h.person(user),
91 93 'comment_file': comment_file,
92 94 'comment_line': comment_line,
93 95 'comment_type': comment_type,
96 'comment_id': comment_id,
94 97 'renderer_type': renderer_type or 'plain',
95 98
96 99 'pr_title': pull_request.title,
97 100 'pr_id': pull_request.pull_request_id,
98 101 'status': status_change,
99 102 'source_ref_type': pull_request.source_ref_parts.type,
100 103 'source_ref_name': pull_request.source_ref_parts.name,
101 104 'target_ref_type': pull_request.target_ref_parts.type,
102 105 'target_ref_name': pull_request.target_ref_parts.name,
103 106 'source_repo': pull_request_source_repo.repo_name,
104 107 'target_repo': pull_request_target_repo.repo_name,
105 108 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
106 109 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
107 110 }
108 111 %>
109 112
110 113 <table style="text-align:left;vertical-align:middle;width: 100%">
111 114 <tr>
112 115 <td style="width:100%;border-bottom:1px solid #dbd9da;">
113 116
114 117 <h4 style="margin: 0">
115 118 <div style="margin-bottom: 4px; color:#7E7F7F">
116 119 @${h.person(user.username)}
117 120 </div>
118 121 ${_('left a')}
119 122 <a href="${pr_comment_url}" style="${base.link_css()}">
120 123 % if comment_file:
121 124 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
122 125 % else:
123 126 ${_('{comment_type} on pull request.').format(**data) |n}
124 127 % endif
125 128 </a>
126 129 <div style="margin-top: 10px"></div>
127 130 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
128 131 </h4>
129 132
130 133 </td>
131 134 </tr>
132 135
133 136 </table>
134 137
135 138 <table style="text-align:left;vertical-align:middle;width: 100%">
136 139
137 140 ## spacing def
138 141 <tr>
139 142 <td style="width: 130px"></td>
140 143 <td></td>
141 144 </tr>
142 145
143 146 % if status_change:
144 147 <tr>
145 148 <td style="padding-right:20px;">${_('Review Status')}:</td>
146 149 <td>
147 150 % if closing_pr:
148 151 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
149 152 % else:
150 153 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
151 154 % endif
152 155 </td>
153 156 </tr>
154 157 % endif
155 158
156 159 <tr>
157 160 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
158 161 <td style="line-height:20px;">
159 162 ${base.tag_button('{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name))} ${_('of')} ${data['source_repo_url']}
160 163 &rarr;
161 164 ${base.tag_button('{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name))} ${_('of')} ${data['target_repo_url']}
162 165 </td>
163 166 </tr>
164 167 <tr>
165 168 <td style="padding-right:20px;">${_('Pull request')}:</td>
166 169 <td>
167 170 <a href="${pull_request_url}" style="${base.link_css()}">
168 171 !${pull_request.pull_request_id}
169 172 </a>
170 173 </td>
171 174 </tr>
172 175 % if comment_file:
173 176 <tr>
174 177 <td style="padding-right:20px;">${_('File')}:</td>
175 178 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
176 179 </tr>
177 180 % endif
178 181
179 <tr style="background-image: linear-gradient(to right, black 33%, rgba(255,255,255,0) 0%);background-position: bottom;background-size: 3px 1px;background-repeat: repeat-x;">
182 <tr style="border-bottom:1px solid #dbd9da;">
180 183 <td colspan="2" style="padding-right:20px;">
181 184 % if comment_type == 'todo':
182 ${_('`TODO` comment')}:
185 ${_('`TODO` number')} ${comment_id}:
183 186 % else:
184 ${_('`Note` comment')}:
187 ${_('`Note` number')} ${comment_id}:
185 188 % endif
186 189 </td>
187 190 </tr>
188 191
189 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
192 <tr>
193 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
194 </tr>
195
196 <tr>
197 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
198 <td></td>
190 199 </tr>
191 200 </table>
@@ -1,804 +1,790 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 7 %if c.rhodecode_name:
8 8 &middot; ${h.branding(c.rhodecode_name)}
9 9 %endif
10 10 </%def>
11 11
12 12 <%def name="breadcrumbs_links()">
13 13 <span id="pr-title">
14 14 ${c.pull_request.title}
15 15 %if c.pull_request.is_closed():
16 16 (${_('Closed')})
17 17 %endif
18 18 </span>
19 19 <div id="pr-title-edit" class="input" style="display: none;">
20 20 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
21 21 </div>
22 22 </%def>
23 23
24 24 <%def name="menu_bar_nav()">
25 25 ${self.menu_items(active='repositories')}
26 26 </%def>
27 27
28 28 <%def name="menu_bar_subnav()">
29 29 ${self.repo_menu(active='showpullrequest')}
30 30 </%def>
31 31
32 32 <%def name="main()">
33 33
34 34 <script type="text/javascript">
35 35 // TODO: marcink switch this to pyroutes
36 36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 38 </script>
39 39 <div class="box">
40 40
41 41 ${self.breadcrumbs()}
42 42
43 43 <div class="box pr-summary">
44 44
45 45 <div class="summary-details block-left">
46 46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 47 <div class="pr-details-title">
48 48 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 49 %if c.allowed_to_update:
50 50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 51 % if c.allowed_to_delete:
52 52 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
53 53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 54 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 55 ${h.end_form()}
56 56 % else:
57 57 ${_('Delete')}
58 58 % endif
59 59 </div>
60 60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 62 %endif
63 63 </div>
64 64
65 65 <div id="summary" class="fields pr-details-content">
66 66 <div class="field">
67 67 <div class="label-summary">
68 68 <label>${_('Source')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 <div class="pr-origininfo">
72 72 ## branch link is only valid if it is a branch
73 73 <span class="tag">
74 74 %if c.pull_request.source_ref_parts.type == 'branch':
75 75 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 76 %else:
77 77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 78 %endif
79 79 </span>
80 80 <span class="clone-url">
81 81 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 82 </span>
83 83 <br/>
84 84 % if c.ancestor_commit:
85 85 ${_('Common ancestor')}:
86 86 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
87 87 % endif
88 88 </div>
89 89 %if h.is_hg(c.pull_request.source_repo):
90 90 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
91 91 %elif h.is_git(c.pull_request.source_repo):
92 92 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
93 93 %endif
94 94
95 95 <div class="">
96 96 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
97 97 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
98 98 </div>
99 99
100 100 </div>
101 101 </div>
102 102 <div class="field">
103 103 <div class="label-summary">
104 104 <label>${_('Target')}:</label>
105 105 </div>
106 106 <div class="input">
107 107 <div class="pr-targetinfo">
108 108 ## branch link is only valid if it is a branch
109 109 <span class="tag">
110 110 %if c.pull_request.target_ref_parts.type == 'branch':
111 111 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
112 112 %else:
113 113 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
114 114 %endif
115 115 </span>
116 116 <span class="clone-url">
117 117 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
118 118 </span>
119 119 </div>
120 120 </div>
121 121 </div>
122 122
123 123 ## Link to the shadow repository.
124 124 <div class="field">
125 125 <div class="label-summary">
126 126 <label>${_('Merge')}:</label>
127 127 </div>
128 128 <div class="input">
129 129 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
134 134 %endif
135 135 <div class="">
136 136 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
137 137 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
138 138 </div>
139 139 % else:
140 140 <div class="">
141 141 ${_('Shadow repository data not available')}.
142 142 </div>
143 143 % endif
144 144 </div>
145 145 </div>
146 146
147 147 <div class="field">
148 148 <div class="label-summary">
149 149 <label>${_('Review')}:</label>
150 150 </div>
151 151 <div class="input">
152 152 %if c.pull_request_review_status:
153 153 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
154 154 <span class="changeset-status-lbl tooltip">
155 155 %if c.pull_request.is_closed():
156 156 ${_('Closed')},
157 157 %endif
158 158 ${h.commit_status_lbl(c.pull_request_review_status)}
159 159 </span>
160 160 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
161 161 %endif
162 162 </div>
163 163 </div>
164 164 <div class="field">
165 165 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
166 166 <label>${_('Description')}:</label>
167 167 </div>
168 168 <div id="pr-desc" class="input">
169 169 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}</div>
170 170 </div>
171 171 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
172 172 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
173 173 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
174 174 </div>
175 175 </div>
176 176
177 177 <div class="field">
178 178 <div class="label-summary">
179 179 <label>${_('Versions')}:</label>
180 180 </div>
181 181
182 182 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
183 183 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
184 184
185 185 <div class="pr-versions">
186 186 % if c.show_version_changes:
187 187 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
188 188 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
189 189 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
190 190 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
191 191 data-toggle-off="${_('Hide all versions of this pull request')}">
192 192 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
193 193 </a>
194 194 <table>
195 195 ## SHOW ALL VERSIONS OF PR
196 196 <% ver_pr = None %>
197 197
198 198 % for data in reversed(list(enumerate(c.versions, 1))):
199 199 <% ver_pos = data[0] %>
200 200 <% ver = data[1] %>
201 201 <% ver_pr = ver.pull_request_version_id %>
202 202 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
203 203
204 204 <tr class="version-pr" style="display: ${display_row}">
205 205 <td>
206 206 <code>
207 207 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
208 208 </code>
209 209 </td>
210 210 <td>
211 211 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
212 212 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
213 213 </td>
214 214 <td>
215 215 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
216 216 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
217 217 </div>
218 218 </td>
219 219 <td>
220 220 % if c.at_version_num != ver_pr:
221 221 <i class="icon-comment"></i>
222 222 <code class="tooltip" title="${_('Comment from pull request version v{0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
223 223 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
224 224 </code>
225 225 % endif
226 226 </td>
227 227 <td>
228 228 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
229 229 </td>
230 230 <td>
231 231 ${h.age_component(ver.updated_on, time_is_local=True)}
232 232 </td>
233 233 </tr>
234 234 % endfor
235 235
236 236 <tr>
237 237 <td colspan="6">
238 238 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
239 239 data-label-text-locked="${_('select versions to show changes')}"
240 240 data-label-text-diff="${_('show changes between versions')}"
241 241 data-label-text-show="${_('show pull request for this version')}"
242 242 >
243 243 ${_('select versions to show changes')}
244 244 </button>
245 245 </td>
246 246 </tr>
247 247 </table>
248 248 % else:
249 249 <div class="input">
250 250 ${_('Pull request versions not available')}.
251 251 </div>
252 252 % endif
253 253 </div>
254 254 </div>
255 255
256 256 <div id="pr-save" class="field" style="display: none;">
257 257 <div class="label-summary"></div>
258 258 <div class="input">
259 259 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
260 260 </div>
261 261 </div>
262 262 </div>
263 263 </div>
264 264 <div>
265 265 ## AUTHOR
266 266 <div class="reviewers-title block-right">
267 267 <div class="pr-details-title">
268 268 ${_('Author of this pull request')}
269 269 </div>
270 270 </div>
271 271 <div class="block-right pr-details-content reviewers">
272 272 <ul class="group_members">
273 273 <li>
274 274 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
275 275 </li>
276 276 </ul>
277 277 </div>
278 278
279 279 ## REVIEW RULES
280 280 <div id="review_rules" style="display: none" class="reviewers-title block-right">
281 281 <div class="pr-details-title">
282 282 ${_('Reviewer rules')}
283 283 %if c.allowed_to_update:
284 284 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
285 285 %endif
286 286 </div>
287 287 <div class="pr-reviewer-rules">
288 288 ## review rules will be appended here, by default reviewers logic
289 289 </div>
290 290 <input id="review_data" type="hidden" name="review_data" value="">
291 291 </div>
292 292
293 293 ## REVIEWERS
294 294 <div class="reviewers-title block-right">
295 295 <div class="pr-details-title">
296 296 ${_('Pull request reviewers')}
297 297 %if c.allowed_to_update:
298 298 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
299 299 %endif
300 300 </div>
301 301 </div>
302 302 <div id="reviewers" class="block-right pr-details-content reviewers">
303 303
304 304 ## members redering block
305 305 <input type="hidden" name="__start__" value="review_members:sequence">
306 306 <ul id="review_members" class="group_members">
307 307
308 308 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
309 309 <script>
310 310 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
311 311 var status = "${(status[0][1].status if status else 'not_reviewed')}";
312 312 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
313 313 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
314 314
315 315 var entry = renderTemplate('reviewMemberEntry', {
316 316 'member': member,
317 317 'mandatory': member.mandatory,
318 318 'reasons': member.reasons,
319 319 'allowed_to_update': allowed_to_update,
320 320 'review_status': status,
321 321 'review_status_label': status_lbl,
322 322 'user_group': member.user_group,
323 323 'create': false
324 324 });
325 325 $('#review_members').append(entry)
326 326 </script>
327 327
328 328 % endfor
329 329
330 330 </ul>
331 331
332 332 <input type="hidden" name="__end__" value="review_members:sequence">
333 333 ## end members redering block
334 334
335 335 %if not c.pull_request.is_closed():
336 336 <div id="add_reviewer" class="ac" style="display: none;">
337 337 %if c.allowed_to_update:
338 338 % if not c.forbid_adding_reviewers:
339 339 <div id="add_reviewer_input" class="reviewer_ac">
340 340 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
341 341 <div id="reviewers_container"></div>
342 342 </div>
343 343 % endif
344 344 <div class="pull-right">
345 345 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
346 346 </div>
347 347 %endif
348 348 </div>
349 349 %endif
350 350 </div>
351 351 </div>
352 352 </div>
353 353 <div class="box">
354 354 ##DIFF
355 355 <div class="table" >
356 356 <div id="changeset_compare_view_content">
357 357 ##CS
358 358 % if c.missing_requirements:
359 359 <div class="box">
360 360 <div class="alert alert-warning">
361 361 <div>
362 362 <strong>${_('Missing requirements:')}</strong>
363 363 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
364 364 </div>
365 365 </div>
366 366 </div>
367 367 % elif c.missing_commits:
368 368 <div class="box">
369 369 <div class="alert alert-warning">
370 370 <div>
371 371 <strong>${_('Missing commits')}:</strong>
372 372 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
373 373 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
374 374 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
375 375 </div>
376 376 </div>
377 377 </div>
378 378 % endif
379 379
380 380 <div class="compare_view_commits_title">
381 381 % if not c.compare_mode:
382 382
383 383 % if c.at_version_pos:
384 384 <h4>
385 385 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
386 386 </h4>
387 387 % endif
388 388
389 389 <div class="pull-left">
390 390 <div class="btn-group">
391 391 <a
392 392 class="btn"
393 393 href="#"
394 394 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
395 395 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
396 396 </a>
397 397 <a
398 398 class="btn"
399 399 href="#"
400 400 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
401 401 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
402 402 </a>
403 403 </div>
404 404 </div>
405 405
406 406 <div class="pull-right">
407 407 % if c.allowed_to_update and not c.pull_request.is_closed():
408 408 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
409 409 % else:
410 410 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
411 411 % endif
412 412
413 413 </div>
414 414 % endif
415 415 </div>
416 416
417 417 % if not c.missing_commits:
418 418 % if c.compare_mode:
419 419 % if c.at_version:
420 420 <h4>
421 421 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
422 422 </h4>
423 423
424 424 <div class="subtitle-compare">
425 425 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
426 426 </div>
427 427
428 428 <div class="container">
429 429 <table class="rctable compare_view_commits">
430 430 <tr>
431 431 <th></th>
432 432 <th>${_('Time')}</th>
433 433 <th>${_('Author')}</th>
434 434 <th>${_('Commit')}</th>
435 435 <th></th>
436 436 <th>${_('Description')}</th>
437 437 </tr>
438 438
439 439 % for c_type, commit in c.commit_changes:
440 440 % if c_type in ['a', 'r']:
441 441 <%
442 442 if c_type == 'a':
443 443 cc_title = _('Commit added in displayed changes')
444 444 elif c_type == 'r':
445 445 cc_title = _('Commit removed in displayed changes')
446 446 else:
447 447 cc_title = ''
448 448 %>
449 449 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
450 450 <td>
451 451 <div class="commit-change-indicator color-${c_type}-border">
452 452 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
453 453 ${c_type.upper()}
454 454 </div>
455 455 </div>
456 456 </td>
457 457 <td class="td-time">
458 458 ${h.age_component(commit.date)}
459 459 </td>
460 460 <td class="td-user">
461 461 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
462 462 </td>
463 463 <td class="td-hash">
464 464 <code>
465 465 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
466 466 r${commit.idx}:${h.short_id(commit.raw_id)}
467 467 </a>
468 468 ${h.hidden('revisions', commit.raw_id)}
469 469 </code>
470 470 </td>
471 471 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
472 472 <i class="icon-expand-linked"></i>
473 473 </td>
474 474 <td class="mid td-description">
475 475 <div class="log-container truncate-wrap">
476 476 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
477 477 </div>
478 478 </td>
479 479 </tr>
480 480 % endif
481 481 % endfor
482 482 </table>
483 483 </div>
484 484
485 485 % endif
486 486
487 487 % else:
488 488 <%include file="/compare/compare_commits.mako" />
489 489 % endif
490 490
491 491 <div class="cs_files">
492 492 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
493 493 % if c.at_version:
494 494 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
495 495 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
496 496 % else:
497 497 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
498 498 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
499 499 % endif
500 500
501 501 <%
502 502 pr_menu_data = {
503 503 'outdated_comm_count_ver': outdated_comm_count_ver
504 504 }
505 505 %>
506 506
507 507 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
508 508
509 509 % if c.range_diff_on:
510 510 % for commit in c.commit_ranges:
511 511 ${cbdiffs.render_diffset(
512 512 c.changes[commit.raw_id],
513 513 commit=commit, use_comments=True,
514 514 collapse_when_files_over=5,
515 515 disable_new_comments=True,
516 516 deleted_files_comments=c.deleted_files_comments,
517 517 inline_comments=c.inline_comments,
518 518 pull_request_menu=pr_menu_data)}
519 519 % endfor
520 520 % else:
521 521 ${cbdiffs.render_diffset(
522 522 c.diffset, use_comments=True,
523 523 collapse_when_files_over=30,
524 524 disable_new_comments=not c.allowed_to_comment,
525 525 deleted_files_comments=c.deleted_files_comments,
526 526 inline_comments=c.inline_comments,
527 527 pull_request_menu=pr_menu_data)}
528 528 % endif
529 529
530 530 </div>
531 531 % else:
532 532 ## skipping commits we need to clear the view for missing commits
533 533 <div style="clear:both;"></div>
534 534 % endif
535 535
536 536 </div>
537 537 </div>
538 538
539 539 ## template for inline comment form
540 540 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
541 541
542 542 ## comments heading with count
543 543 <div class="comments-heading">
544 544 <i class="icon-comment"></i>
545 545 ${_('Comments')} ${len(c.comments)}
546 546 </div>
547 547
548 548 ## render general comments
549 549 <div id="comment-tr-show">
550 550 % if general_outdated_comm_count_ver:
551 551 <div class="info-box">
552 552 % if general_outdated_comm_count_ver == 1:
553 553 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
554 554 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
555 555 % else:
556 556 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
557 557 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
558 558 % endif
559 559 </div>
560 560 % endif
561 561 </div>
562 562
563 563 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
564 564
565 565 % if not c.pull_request.is_closed():
566 566 ## merge status, and merge action
567 567 <div class="pull-request-merge">
568 568 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
569 569 </div>
570 570
571 571 ## main comment form and it status
572 572 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
573 573 pull_request_id=c.pull_request.pull_request_id),
574 574 c.pull_request_review_status,
575 575 is_pull_request=True, change_status=c.allowed_to_change_status)}
576 576 %endif
577 577
578 578 <script type="text/javascript">
579 if (location.hash) {
580 var result = splitDelimitedHash(location.hash);
581 var line = $('html').find(result.loc);
582 // show hidden comments if we use location.hash
583 if (line.hasClass('comment-general')) {
584 $(line).show();
585 } else if (line.hasClass('comment-inline')) {
586 $(line).show();
587 var $cb = $(line).closest('.cb');
588 $cb.removeClass('cb-collapsed')
589 }
590 if (line.length > 0){
591 offsetScroll(line, 70);
592 }
593 }
594 579
595 580 versionController = new VersionController();
596 581 versionController.init();
597 582
598 583 reviewersController = new ReviewersController();
599 584 commitsController = new CommitsController();
600 585
601 586 $(function(){
602 587
603 588 // custom code mirror
604 589 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
605 590
606 591 var PRDetails = {
607 592 editButton: $('#open_edit_pullrequest'),
608 593 closeButton: $('#close_edit_pullrequest'),
609 594 deleteButton: $('#delete_pullrequest'),
610 595 viewFields: $('#pr-desc, #pr-title'),
611 596 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
612 597
613 598 init: function() {
614 599 var that = this;
615 600 this.editButton.on('click', function(e) { that.edit(); });
616 601 this.closeButton.on('click', function(e) { that.view(); });
617 602 },
618 603
619 604 edit: function(event) {
620 605 this.viewFields.hide();
621 606 this.editButton.hide();
622 607 this.deleteButton.hide();
623 608 this.closeButton.show();
624 609 this.editFields.show();
625 610 codeMirrorInstance.refresh();
626 611 },
627 612
628 613 view: function(event) {
629 614 this.editButton.show();
630 615 this.deleteButton.show();
631 616 this.editFields.hide();
632 617 this.closeButton.hide();
633 618 this.viewFields.show();
634 619 }
635 620 };
636 621
637 622 var ReviewersPanel = {
638 623 editButton: $('#open_edit_reviewers'),
639 624 closeButton: $('#close_edit_reviewers'),
640 625 addButton: $('#add_reviewer'),
641 626 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
642 627
643 628 init: function() {
644 629 var self = this;
645 630 this.editButton.on('click', function(e) { self.edit(); });
646 631 this.closeButton.on('click', function(e) { self.close(); });
647 632 },
648 633
649 634 edit: function(event) {
650 635 this.editButton.hide();
651 636 this.closeButton.show();
652 637 this.addButton.show();
653 638 this.removeButtons.css('visibility', 'visible');
654 639 // review rules
655 640 reviewersController.loadReviewRules(
656 641 ${c.pull_request.reviewer_data_json | n});
657 642 },
658 643
659 644 close: function(event) {
660 645 this.editButton.show();
661 646 this.closeButton.hide();
662 647 this.addButton.hide();
663 648 this.removeButtons.css('visibility', 'hidden');
664 649 // hide review rules
665 650 reviewersController.hideReviewRules()
666 651 }
667 652 };
668 653
669 654 PRDetails.init();
670 655 ReviewersPanel.init();
671 656
672 657 showOutdated = function(self){
673 658 $('.comment-inline.comment-outdated').show();
674 659 $('.filediff-outdated').show();
675 660 $('.showOutdatedComments').hide();
676 661 $('.hideOutdatedComments').show();
677 662 };
678 663
679 664 hideOutdated = function(self){
680 665 $('.comment-inline.comment-outdated').hide();
681 666 $('.filediff-outdated').hide();
682 667 $('.hideOutdatedComments').hide();
683 668 $('.showOutdatedComments').show();
684 669 };
685 670
686 671 refreshMergeChecks = function(){
687 672 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
688 673 $('.pull-request-merge').css('opacity', 0.3);
689 674 $('.action-buttons-extra').css('opacity', 0.3);
690 675
691 676 $('.pull-request-merge').load(
692 677 loadUrl, function() {
693 678 $('.pull-request-merge').css('opacity', 1);
694 679
695 680 $('.action-buttons-extra').css('opacity', 1);
696 681 }
697 682 );
698 683 };
699 684
700 685 closePullRequest = function (status) {
701 686 // inject closing flag
702 687 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
703 688 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
704 689 $(generalCommentForm.submitForm).submit();
705 690 };
706 691
707 692 $('#show-outdated-comments').on('click', function(e){
708 693 var button = $(this);
709 694 var outdated = $('.comment-outdated');
710 695
711 696 if (button.html() === "(Show)") {
712 697 button.html("(Hide)");
713 698 outdated.show();
714 699 } else {
715 700 button.html("(Show)");
716 701 outdated.hide();
717 702 }
718 703 });
719 704
720 705 $('.show-inline-comments').on('change', function(e){
721 706 var show = 'none';
722 707 var target = e.currentTarget;
723 708 if(target.checked){
724 709 show = ''
725 710 }
726 711 var boxid = $(target).attr('id_for');
727 712 var comments = $('#{0} .inline-comments'.format(boxid));
728 713 var fn_display = function(idx){
729 714 $(this).css('display', show);
730 715 };
731 716 $(comments).each(fn_display);
732 717 var btns = $('#{0} .inline-comments-button'.format(boxid));
733 718 $(btns).each(fn_display);
734 719 });
735 720
736 721 $('#merge_pull_request_form').submit(function() {
737 722 if (!$('#merge_pull_request').attr('disabled')) {
738 723 $('#merge_pull_request').attr('disabled', 'disabled');
739 724 }
740 725 return true;
741 726 });
742 727
743 728 $('#edit_pull_request').on('click', function(e){
744 729 var title = $('#pr-title-input').val();
745 730 var description = codeMirrorInstance.getValue();
746 731 var renderer = $('#pr-renderer-input').val();
747 732 editPullRequest(
748 733 "${c.repo_name}", "${c.pull_request.pull_request_id}",
749 734 title, description, renderer);
750 735 });
751 736
752 737 $('#update_pull_request').on('click', function(e){
753 738 $(this).attr('disabled', 'disabled');
754 739 $(this).addClass('disabled');
755 740 $(this).html(_gettext('Saving...'));
756 741 reviewersController.updateReviewers(
757 742 "${c.repo_name}", "${c.pull_request.pull_request_id}");
758 743 });
759 744
760 745 $('#update_commits').on('click', function(e){
761 746 var isDisabled = !$(e.currentTarget).attr('disabled');
762 747 $(e.currentTarget).attr('disabled', 'disabled');
763 748 $(e.currentTarget).addClass('disabled');
764 749 $(e.currentTarget).removeClass('btn-primary');
765 750 $(e.currentTarget).text(_gettext('Updating...'));
766 751 if(isDisabled){
767 752 updateCommits(
768 753 "${c.repo_name}", "${c.pull_request.pull_request_id}");
769 754 }
770 755 });
771 756 // fixing issue with caches on firefox
772 757 $('#update_commits').removeAttr("disabled");
773 758
774 759 $('.show-inline-comments').on('click', function(e){
775 760 var boxid = $(this).attr('data-comment-id');
776 761 var button = $(this);
777 762
778 763 if(button.hasClass("comments-visible")) {
779 764 $('#{0} .inline-comments'.format(boxid)).each(function(index){
780 765 $(this).hide();
781 766 });
782 767 button.removeClass("comments-visible");
783 768 } else {
784 769 $('#{0} .inline-comments'.format(boxid)).each(function(index){
785 770 $(this).show();
786 771 });
787 772 button.addClass("comments-visible");
788 773 }
789 774 });
790 775
791 776 // register submit callback on commentForm form to track TODOs
792 777 window.commentFormGlobalSubmitSuccessCallback = function(){
793 778 refreshMergeChecks();
794 779 };
795 780
796 781 ReviewerAutoComplete('#user');
797 782
798 783 })
784
799 785 </script>
800 786
801 787 </div>
802 788 </div>
803 789
804 790 </%def>
@@ -1,138 +1,141 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22 import collections
23 23
24 24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 25 from rhodecode.lib.utils2 import AttributeDict
26 26 from rhodecode.model.db import User
27 27 from rhodecode.model.notification import EmailNotificationModel
28 28
29 29
30 30 def test_get_template_obj(app, request_stub):
31 31 template = EmailNotificationModel().get_renderer(
32 32 EmailNotificationModel.TYPE_TEST, request_stub)
33 33 assert isinstance(template, PyramidPartialRenderer)
34 34
35 35
36 36 def test_render_email(app, http_host_only_stub):
37 37 kwargs = {}
38 38 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
39 39 EmailNotificationModel.TYPE_TEST, **kwargs)
40 40
41 41 # subject
42 42 assert subject == 'Test "Subject" hello "world"'
43 43
44 44 # headers
45 45 assert headers == 'X=Y'
46 46
47 47 # body plaintext
48 48 assert body_plaintext == 'Email Plaintext Body'
49 49
50 50 # body
51 51 notification_footer1 = 'This is a notification from RhodeCode.'
52 52 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
53 53 assert notification_footer1 in body
54 54 assert notification_footer2 in body
55 55 assert 'Email Body' in body
56 56
57 57
58 58 def test_render_pr_email(app, user_admin):
59 59 ref = collections.namedtuple(
60 60 'Ref', 'name, type')('fxies123', 'book')
61 61
62 62 pr = collections.namedtuple('PullRequest',
63 63 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
64 64 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
65 65
66 66 source_repo = target_repo = collections.namedtuple(
67 67 'Repo', 'type, repo_name')('hg', 'pull_request_1')
68 68
69 69 kwargs = {
70 70 'user': User.get_first_super_admin(),
71 71 'pull_request': pr,
72 72 'pull_request_commits': [],
73 73
74 74 'pull_request_target_repo': target_repo,
75 75 'pull_request_target_repo_url': 'x',
76 76
77 77 'pull_request_source_repo': source_repo,
78 78 'pull_request_source_repo_url': 'x',
79 79
80 80 'pull_request_url': 'http://localhost/pr1',
81 81 }
82 82
83 83 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
84 84 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
85 85
86 86 # subject
87 87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 88
89 89
90 90 @pytest.mark.parametrize('mention', [
91 91 True,
92 92 False
93 93 ])
94 94 @pytest.mark.parametrize('email_type', [
95 95 EmailNotificationModel.TYPE_COMMIT_COMMENT,
96 96 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
97 97 ])
98 98 def test_render_comment_subject_no_newlines(app, mention, email_type):
99 99 ref = collections.namedtuple(
100 100 'Ref', 'name, type')('fxies123', 'book')
101 101
102 102 pr = collections.namedtuple('PullRequest',
103 103 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
104 104 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
105 105
106 106 source_repo = target_repo = collections.namedtuple(
107 107 'Repo', 'type, repo_name')('hg', 'pull_request_1')
108 108
109 109 kwargs = {
110 110 'user': User.get_first_super_admin(),
111 111 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
112 112 'status_change': 'approved',
113 113 'commit_target_repo_url': 'http://foo.example.com/#comment1',
114 114 'repo_name': 'test-repo',
115 115 'comment_file': 'test-file.py',
116 116 'comment_line': 'n100',
117 117 'comment_type': 'note',
118 'comment_id': 2048,
118 119 'commit_comment_url': 'http://comment-url',
120 'commit_comment_reply_url': 'http://comment-url/#Reply',
119 121 'instance_url': 'http://rc-instance',
120 122 'comment_body': 'hello world',
121 123 'mention': mention,
122 124
123 125 'pr_comment_url': 'http://comment-url',
126 'pr_comment_reply_url': 'http://comment-url/#Reply',
124 127 'pull_request': pr,
125 128 'pull_request_commits': [],
126 129
127 130 'pull_request_target_repo': target_repo,
128 131 'pull_request_target_repo_url': 'x',
129 132
130 133 'pull_request_source_repo': source_repo,
131 134 'pull_request_source_repo_url': 'x',
132 135
133 136 'pull_request_url': 'http://code.rc.com/_pr/123'
134 137 }
135 138 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
136 139 email_type, **kwargs)
137 140
138 141 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now