##// END OF EJS Templates
imported patch 2260
marcink -
r2162:04d3fae0 beta
parent child Browse files
Show More
@@ -1,602 +1,603 b''
1 1 .. _changelog:
2 2
3 3 =========
4 4 Changelog
5 5 =========
6 6
7 7
8 8 1.3.4 (**2012-XX-XX**)
9 9 ----------------------
10 10
11 11 :status: in-progress
12 12 :branch: beta
13 13
14 14 news
15 15 ++++
16 16
17 17 - Whoosh logging is now controlled by the .ini files logging setup
18 18 - added clone-url into edit form on /settings page
19 19 - added help text into repo add/edit forms
20 20 - created rcextensions module with additional mappings (ref #322) and
21 21 post push/pull/create repo hooks callbacks
22 22 - implemented #377 Users view for his own permissions on account page
23 23 - #399 added inheritance of permissions for users group on repos groups
24 24 - #401 repository group is automatically pre-selected when adding repos
25 25 inside a repository group
26 26 - added alternative HTTP 403 response when client failed to authenticate. Helps
27 27 solving issues with Mercurial and LDAP
28 28 - #402 removed group prefix from repository name when listing repositories
29 29 inside a group
30 30 - added gravatars into permission view and permissions autocomplete
31 31 - #347 when running multiple RhodeCode instances, properly invalidates cache
32 32 for all registered servers
33 33
34 34 fixes
35 35 +++++
36 36
37 37 - fixed #390 cache invalidation problems on repos inside group
38 38 - fixed #385 clone by ID url was loosing proxy prefix in URL
39 39 - fixed some unicode problems with waitress
40 40 - fixed issue with escaping < and > in changeset commits
41 41 - fixed error occurring during recursive group creation in API
42 42 create_repo function
43 43 - fixed #393 py2.5 fixes for routes url generator
44 44 - fixed #397 Private repository groups shows up before login
45 45 - fixed #396 fixed problems with revoking users in nested groups
46 46 - fixed mysql unicode issues + specified InnoDB as default engine with
47 47 utf8 charset
48 - #406 trim long branch/tag names in changelog to not break UI
48 49
49 50 1.3.3 (**2012-03-02**)
50 51 ----------------------
51 52
52 53 news
53 54 ++++
54 55
55 56
56 57 fixes
57 58 +++++
58 59
59 60 - fixed some python2.5 compatibility issues
60 61 - fixed issues with removed repos was accidentally added as groups, after
61 62 full rescan of paths
62 63 - fixes #376 Cannot edit user (using container auth)
63 64 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
64 65 configuration
65 66 - fixed initial sorting of repos inside repo group
66 67 - fixes issue when user tried to resubmit same permission into user/user_groups
67 68 - bumped beaker version that fixes #375 leap error bug
68 69 - fixed raw_changeset for git. It was generated with hg patch headers
69 70 - fixed vcs issue with last_changeset for filenodes
70 71 - fixed missing commit after hook delete
71 72 - fixed #372 issues with git operation detection that caused a security issue
72 73 for git repos
73 74
74 75 1.3.2 (**2012-02-28**)
75 76 ----------------------
76 77
77 78 news
78 79 ++++
79 80
80 81
81 82 fixes
82 83 +++++
83 84
84 85 - fixed git protocol issues with repos-groups
85 86 - fixed git remote repos validator that prevented from cloning remote git repos
86 87 - fixes #370 ending slashes fixes for repo and groups
87 88 - fixes #368 improved git-protocol detection to handle other clients
88 89 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
89 90 Moved To Root
90 91 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
91 92 - fixed #373 missing cascade drop on user_group_to_perm table
92 93
93 94 1.3.1 (**2012-02-27**)
94 95 ----------------------
95 96
96 97 news
97 98 ++++
98 99
99 100
100 101 fixes
101 102 +++++
102 103
103 104 - redirection loop occurs when remember-me wasn't checked during login
104 105 - fixes issues with git blob history generation
105 106 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
106 107
107 108 1.3.0 (**2012-02-26**)
108 109 ----------------------
109 110
110 111 news
111 112 ++++
112 113
113 114 - code review, inspired by github code-comments
114 115 - #215 rst and markdown README files support
115 116 - #252 Container-based and proxy pass-through authentication support
116 117 - #44 branch browser. Filtering of changelog by branches
117 118 - mercurial bookmarks support
118 119 - new hover top menu, optimized to add maximum size for important views
119 120 - configurable clone url template with possibility to specify protocol like
120 121 ssh:// or http:// and also manually alter other parts of clone_url.
121 122 - enabled largefiles extension by default
122 123 - optimized summary file pages and saved a lot of unused space in them
123 124 - #239 option to manually mark repository as fork
124 125 - #320 mapping of commit authors to RhodeCode users
125 126 - #304 hashes are displayed using monospace font
126 127 - diff configuration, toggle white lines and context lines
127 128 - #307 configurable diffs, whitespace toggle, increasing context lines
128 129 - sorting on branches, tags and bookmarks using YUI datatable
129 130 - improved file filter on files page
130 131 - implements #330 api method for listing nodes ar particular revision
131 132 - #73 added linking issues in commit messages to chosen issue tracker url
132 133 based on user defined regular expression
133 134 - added linking of changesets in commit messages
134 135 - new compact changelog with expandable commit messages
135 136 - firstname and lastname are optional in user creation
136 137 - #348 added post-create repository hook
137 138 - #212 global encoding settings is now configurable from .ini files
138 139 - #227 added repository groups permissions
139 140 - markdown gets codehilite extensions
140 141 - new API methods, delete_repositories, grante/revoke permissions for groups
141 142 and repos
142 143
143 144
144 145 fixes
145 146 +++++
146 147
147 148 - rewrote dbsession management for atomic operations, and better error handling
148 149 - fixed sorting of repo tables
149 150 - #326 escape of special html entities in diffs
150 151 - normalized user_name => username in api attributes
151 152 - fixes #298 ldap created users with mixed case emails created conflicts
152 153 on saving a form
153 154 - fixes issue when owner of a repo couldn't revoke permissions for users
154 155 and groups
155 156 - fixes #271 rare JSON serialization problem with statistics
156 157 - fixes #337 missing validation check for conflicting names of a group with a
157 158 repositories group
158 159 - #340 fixed session problem for mysql and celery tasks
159 160 - fixed #331 RhodeCode mangles repository names if the a repository group
160 161 contains the "full path" to the repositories
161 162 - #355 RhodeCode doesn't store encrypted LDAP passwords
162 163
163 164 1.2.5 (**2012-01-28**)
164 165 ----------------------
165 166
166 167 news
167 168 ++++
168 169
169 170 fixes
170 171 +++++
171 172
172 173 - #340 Celery complains about MySQL server gone away, added session cleanup
173 174 for celery tasks
174 175 - #341 "scanning for repositories in None" log message during Rescan was missing
175 176 a parameter
176 177 - fixed creating archives with subrepos. Some hooks were triggered during that
177 178 operation leading to crash.
178 179 - fixed missing email in account page.
179 180 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
180 181 forking on windows impossible
181 182
182 183 1.2.4 (**2012-01-19**)
183 184 ----------------------
184 185
185 186 news
186 187 ++++
187 188
188 189 - RhodeCode is bundled with mercurial series 2.0.X by default, with
189 190 full support to largefiles extension. Enabled by default in new installations
190 191 - #329 Ability to Add/Remove Groups to/from a Repository via AP
191 192 - added requires.txt file with requirements
192 193
193 194 fixes
194 195 +++++
195 196
196 197 - fixes db session issues with celery when emailing admins
197 198 - #331 RhodeCode mangles repository names if the a repository group
198 199 contains the "full path" to the repositories
199 200 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
200 201 - DB session cleanup after hg protocol operations, fixes issues with
201 202 `mysql has gone away` errors
202 203 - #333 doc fixes for get_repo api function
203 204 - #271 rare JSON serialization problem with statistics enabled
204 205 - #337 Fixes issues with validation of repository name conflicting with
205 206 a group name. A proper message is now displayed.
206 207 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
207 208 doesn't work
208 209 - #316 fixes issues with web description in hgrc files
209 210
210 211 1.2.3 (**2011-11-02**)
211 212 ----------------------
212 213
213 214 news
214 215 ++++
215 216
216 217 - added option to manage repos group for non admin users
217 218 - added following API methods for get_users, create_user, get_users_groups,
218 219 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
219 220 get_repo, create_repo, add_user_to_repo
220 221 - implements #237 added password confirmation for my account
221 222 and admin edit user.
222 223 - implements #291 email notification for global events are now sent to all
223 224 administrator users, and global config email.
224 225
225 226 fixes
226 227 +++++
227 228
228 229 - added option for passing auth method for smtp mailer
229 230 - #276 issue with adding a single user with id>10 to usergroups
230 231 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
231 232 - #288 fixes managing of repos in a group for non admin user
232 233
233 234 1.2.2 (**2011-10-17**)
234 235 ----------------------
235 236
236 237 news
237 238 ++++
238 239
239 240 - #226 repo groups are available by path instead of numerical id
240 241
241 242 fixes
242 243 +++++
243 244
244 245 - #259 Groups with the same name but with different parent group
245 246 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
246 247 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
247 248 - #265 ldap save fails sometimes on converting attributes to booleans,
248 249 added getter and setter into model that will prevent from this on db model level
249 250 - fixed problems with timestamps issues #251 and #213
250 251 - fixes #266 RhodeCode allows to create repo with the same name and in
251 252 the same parent as group
252 253 - fixes #245 Rescan of the repositories on Windows
253 254 - fixes #248 cannot edit repos inside a group on windows
254 255 - fixes #219 forking problems on windows
255 256
256 257 1.2.1 (**2011-10-08**)
257 258 ----------------------
258 259
259 260 news
260 261 ++++
261 262
262 263
263 264 fixes
264 265 +++++
265 266
266 267 - fixed problems with basic auth and push problems
267 268 - gui fixes
268 269 - fixed logger
269 270
270 271 1.2.0 (**2011-10-07**)
271 272 ----------------------
272 273
273 274 news
274 275 ++++
275 276
276 277 - implemented #47 repository groups
277 278 - implemented #89 Can setup google analytics code from settings menu
278 279 - implemented #91 added nicer looking archive urls with more download options
279 280 like tags, branches
280 281 - implemented #44 into file browsing, and added follow branch option
281 282 - implemented #84 downloads can be enabled/disabled for each repository
282 283 - anonymous repository can be cloned without having to pass default:default
283 284 into clone url
284 285 - fixed #90 whoosh indexer can index chooses repositories passed in command
285 286 line
286 287 - extended journal with day aggregates and paging
287 288 - implemented #107 source code lines highlight ranges
288 289 - implemented #93 customizable changelog on combined revision ranges -
289 290 equivalent of githubs compare view
290 291 - implemented #108 extended and more powerful LDAP configuration
291 292 - implemented #56 users groups
292 293 - major code rewrites optimized codes for speed and memory usage
293 294 - raw and diff downloads are now in git format
294 295 - setup command checks for write access to given path
295 296 - fixed many issues with international characters and unicode. It uses utf8
296 297 decode with replace to provide less errors even with non utf8 encoded strings
297 298 - #125 added API KEY access to feeds
298 299 - #109 Repository can be created from external Mercurial link (aka. remote
299 300 repository, and manually updated (via pull) from admin panel
300 301 - beta git support - push/pull server + basic view for git repos
301 302 - added followers page and forks page
302 303 - server side file creation (with binary file upload interface)
303 304 and edition with commits powered by codemirror
304 305 - #111 file browser file finder, quick lookup files on whole file tree
305 306 - added quick login sliding menu into main page
306 307 - changelog uses lazy loading of affected files details, in some scenarios
307 308 this can improve speed of changelog page dramatically especially for
308 309 larger repositories.
309 310 - implements #214 added support for downloading subrepos in download menu.
310 311 - Added basic API for direct operations on rhodecode via JSON
311 312 - Implemented advanced hook management
312 313
313 314 fixes
314 315 +++++
315 316
316 317 - fixed file browser bug, when switching into given form revision the url was
317 318 not changing
318 319 - fixed propagation to error controller on simplehg and simplegit middlewares
319 320 - fixed error when trying to make a download on empty repository
320 321 - fixed problem with '[' chars in commit messages in journal
321 322 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
322 323 - journal fork fixes
323 324 - removed issue with space inside renamed repository after deletion
324 325 - fixed strange issue on formencode imports
325 326 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
326 327 - #150 fixes for errors on repositories mapped in db but corrupted in
327 328 filesystem
328 329 - fixed problem with ascendant characters in realm #181
329 330 - fixed problem with sqlite file based database connection pool
330 331 - whoosh indexer and code stats share the same dynamic extensions map
331 332 - fixes #188 - relationship delete of repo_to_perm entry on user removal
332 333 - fixes issue #189 Trending source files shows "show more" when no more exist
333 334 - fixes issue #197 Relative paths for pidlocks
334 335 - fixes issue #198 password will require only 3 chars now for login form
335 336 - fixes issue #199 wrong redirection for non admin users after creating a repository
336 337 - fixes issues #202, bad db constraint made impossible to attach same group
337 338 more than one time. Affects only mysql/postgres
338 339 - fixes #218 os.kill patch for windows was missing sig param
339 340 - improved rendering of dag (they are not trimmed anymore when number of
340 341 heads exceeds 5)
341 342
342 343 1.1.8 (**2011-04-12**)
343 344 ----------------------
344 345
345 346 news
346 347 ++++
347 348
348 349 - improved windows support
349 350
350 351 fixes
351 352 +++++
352 353
353 354 - fixed #140 freeze of python dateutil library, since new version is python2.x
354 355 incompatible
355 356 - setup-app will check for write permission in given path
356 357 - cleaned up license info issue #149
357 358 - fixes for issues #137,#116 and problems with unicode and accented characters.
358 359 - fixes crashes on gravatar, when passed in email as unicode
359 360 - fixed tooltip flickering problems
360 361 - fixed came_from redirection on windows
361 362 - fixed logging modules, and sql formatters
362 363 - windows fixes for os.kill issue #133
363 364 - fixes path splitting for windows issues #148
364 365 - fixed issue #143 wrong import on migration to 1.1.X
365 366 - fixed problems with displaying binary files, thanks to Thomas Waldmann
366 367 - removed name from archive files since it's breaking ui for long repo names
367 368 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
368 369 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
369 370 Thomas Waldmann
370 371 - fixed issue #166 summary pager was skipping 10 revisions on second page
371 372
372 373
373 374 1.1.7 (**2011-03-23**)
374 375 ----------------------
375 376
376 377 news
377 378 ++++
378 379
379 380 fixes
380 381 +++++
381 382
382 383 - fixed (again) #136 installation support for FreeBSD
383 384
384 385
385 386 1.1.6 (**2011-03-21**)
386 387 ----------------------
387 388
388 389 news
389 390 ++++
390 391
391 392 fixes
392 393 +++++
393 394
394 395 - fixed #136 installation support for FreeBSD
395 396 - RhodeCode will check for python version during installation
396 397
397 398 1.1.5 (**2011-03-17**)
398 399 ----------------------
399 400
400 401 news
401 402 ++++
402 403
403 404 - basic windows support, by exchanging pybcrypt into sha256 for windows only
404 405 highly inspired by idea of mantis406
405 406
406 407 fixes
407 408 +++++
408 409
409 410 - fixed sorting by author in main page
410 411 - fixed crashes with diffs on binary files
411 412 - fixed #131 problem with boolean values for LDAP
412 413 - fixed #122 mysql problems thanks to striker69
413 414 - fixed problem with errors on calling raw/raw_files/annotate functions
414 415 with unknown revisions
415 416 - fixed returned rawfiles attachment names with international character
416 417 - cleaned out docs, big thanks to Jason Harris
417 418
418 419 1.1.4 (**2011-02-19**)
419 420 ----------------------
420 421
421 422 news
422 423 ++++
423 424
424 425 fixes
425 426 +++++
426 427
427 428 - fixed formencode import problem on settings page, that caused server crash
428 429 when that page was accessed as first after server start
429 430 - journal fixes
430 431 - fixed option to access repository just by entering http://server/<repo_name>
431 432
432 433 1.1.3 (**2011-02-16**)
433 434 ----------------------
434 435
435 436 news
436 437 ++++
437 438
438 439 - implemented #102 allowing the '.' character in username
439 440 - added option to access repository just by entering http://server/<repo_name>
440 441 - celery task ignores result for better performance
441 442
442 443 fixes
443 444 +++++
444 445
445 446 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
446 447 apollo13 and Johan Walles
447 448 - small fixes in journal
448 449 - fixed problems with getting setting for celery from .ini files
449 450 - registration, password reset and login boxes share the same title as main
450 451 application now
451 452 - fixed #113: to high permissions to fork repository
452 453 - fixed problem with '[' chars in commit messages in journal
453 454 - removed issue with space inside renamed repository after deletion
454 455 - db transaction fixes when filesystem repository creation failed
455 456 - fixed #106 relation issues on databases different than sqlite
456 457 - fixed static files paths links to use of url() method
457 458
458 459 1.1.2 (**2011-01-12**)
459 460 ----------------------
460 461
461 462 news
462 463 ++++
463 464
464 465
465 466 fixes
466 467 +++++
467 468
468 469 - fixes #98 protection against float division of percentage stats
469 470 - fixed graph bug
470 471 - forced webhelpers version since it was making troubles during installation
471 472
472 473 1.1.1 (**2011-01-06**)
473 474 ----------------------
474 475
475 476 news
476 477 ++++
477 478
478 479 - added force https option into ini files for easier https usage (no need to
479 480 set server headers with this options)
480 481 - small css updates
481 482
482 483 fixes
483 484 +++++
484 485
485 486 - fixed #96 redirect loop on files view on repositories without changesets
486 487 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
487 488 and server crashed with errors
488 489 - fixed large tooltips problems on main page
489 490 - fixed #92 whoosh indexer is more error proof
490 491
491 492 1.1.0 (**2010-12-18**)
492 493 ----------------------
493 494
494 495 news
495 496 ++++
496 497
497 498 - rewrite of internals for vcs >=0.1.10
498 499 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
499 500 with older clients
500 501 - anonymous access, authentication via ldap
501 502 - performance upgrade for cached repos list - each repository has its own
502 503 cache that's invalidated when needed.
503 504 - performance upgrades on repositories with large amount of commits (20K+)
504 505 - main page quick filter for filtering repositories
505 506 - user dashboards with ability to follow chosen repositories actions
506 507 - sends email to admin on new user registration
507 508 - added cache/statistics reset options into repository settings
508 509 - more detailed action logger (based on hooks) with pushed changesets lists
509 510 and options to disable those hooks from admin panel
510 511 - introduced new enhanced changelog for merges that shows more accurate results
511 512 - new improved and faster code stats (based on pygments lexers mapping tables,
512 513 showing up to 10 trending sources for each repository. Additionally stats
513 514 can be disabled in repository settings.
514 515 - gui optimizations, fixed application width to 1024px
515 516 - added cut off (for large files/changesets) limit into config files
516 517 - whoosh, celeryd, upgrade moved to paster command
517 518 - other than sqlite database backends can be used
518 519
519 520 fixes
520 521 +++++
521 522
522 523 - fixes #61 forked repo was showing only after cache expired
523 524 - fixes #76 no confirmation on user deletes
524 525 - fixes #66 Name field misspelled
525 526 - fixes #72 block user removal when he owns repositories
526 527 - fixes #69 added password confirmation fields
527 528 - fixes #87 RhodeCode crashes occasionally on updating repository owner
528 529 - fixes #82 broken annotations on files with more than 1 blank line at the end
529 530 - a lot of fixes and tweaks for file browser
530 531 - fixed detached session issues
531 532 - fixed when user had no repos he would see all repos listed in my account
532 533 - fixed ui() instance bug when global hgrc settings was loaded for server
533 534 instance and all hgrc options were merged with our db ui() object
534 535 - numerous small bugfixes
535 536
536 537 (special thanks for TkSoh for detailed feedback)
537 538
538 539
539 540 1.0.2 (**2010-11-12**)
540 541 ----------------------
541 542
542 543 news
543 544 ++++
544 545
545 546 - tested under python2.7
546 547 - bumped sqlalchemy and celery versions
547 548
548 549 fixes
549 550 +++++
550 551
551 552 - fixed #59 missing graph.js
552 553 - fixed repo_size crash when repository had broken symlinks
553 554 - fixed python2.5 crashes.
554 555
555 556
556 557 1.0.1 (**2010-11-10**)
557 558 ----------------------
558 559
559 560 news
560 561 ++++
561 562
562 563 - small css updated
563 564
564 565 fixes
565 566 +++++
566 567
567 568 - fixed #53 python2.5 incompatible enumerate calls
568 569 - fixed #52 disable mercurial extension for web
569 570 - fixed #51 deleting repositories don't delete it's dependent objects
570 571
571 572
572 573 1.0.0 (**2010-11-02**)
573 574 ----------------------
574 575
575 576 - security bugfix simplehg wasn't checking for permissions on commands
576 577 other than pull or push.
577 578 - fixed doubled messages after push or pull in admin journal
578 579 - templating and css corrections, fixed repo switcher on chrome, updated titles
579 580 - admin menu accessible from options menu on repository view
580 581 - permissions cached queries
581 582
582 583 1.0.0rc4 (**2010-10-12**)
583 584 --------------------------
584 585
585 586 - fixed python2.5 missing simplejson imports (thanks to Jens BΓ€ckman)
586 587 - removed cache_manager settings from sqlalchemy meta
587 588 - added sqlalchemy cache settings to ini files
588 589 - validated password length and added second try of failure on paster setup-app
589 590 - fixed setup database destroy prompt even when there was no db
590 591
591 592
592 593 1.0.0rc3 (**2010-10-11**)
593 594 -------------------------
594 595
595 596 - fixed i18n during installation.
596 597
597 598 1.0.0rc2 (**2010-10-11**)
598 599 -------------------------
599 600
600 601 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
601 602 occure. After vcs is fixed it'll be put back again.
602 603 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,926 +1,938 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11 import logging
12 12
13 13 from datetime import datetime
14 14 from pygments.formatters.html import HtmlFormatter
15 15 from pygments import highlight as code_highlight
16 16 from pylons import url, request, config
17 17 from pylons.i18n.translation import _, ungettext
18 18 from hashlib import md5
19 19
20 20 from webhelpers.html import literal, HTML, escape
21 21 from webhelpers.html.tools import *
22 22 from webhelpers.html.builder import make_tag
23 23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
24 24 end_form, file, form, hidden, image, javascript_link, link_to, \
25 25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
26 26 submit, text, password, textarea, title, ul, xml_declaration, radio
27 27 from webhelpers.html.tools import auto_link, button_to, highlight, \
28 28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
29 29 from webhelpers.number import format_byte_size, format_bit_size
30 30 from webhelpers.pylonslib import Flash as _Flash
31 31 from webhelpers.pylonslib.secure_form import secure_form
32 32 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
33 33 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
34 34 replace_whitespace, urlify, truncate, wrap_paragraphs
35 35 from webhelpers.date import time_ago_in_words
36 36 from webhelpers.paginate import Page
37 37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
38 38 convert_boolean_attrs, NotGiven, _make_safe_id_component
39 39
40 40 from rhodecode.lib.annotate import annotate_highlight
41 41 from rhodecode.lib.utils import repo_name_slug
42 42 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
43 43 get_changeset_safe
44 44 from rhodecode.lib.markup_renderer import MarkupRenderer
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 def shorter(text, size=20):
50 postfix = '...'
51 if len(text) > size:
52 return text[:size - len(postfix)] + postfix
53 return text
54
55
49 56 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
50 57 """
51 58 Reset button
52 59 """
53 60 _set_input_attrs(attrs, type, name, value)
54 61 _set_id_attr(attrs, id, name)
55 62 convert_boolean_attrs(attrs, ["disabled"])
56 63 return HTML.input(**attrs)
57 64
58 65 reset = _reset
59 66 safeid = _make_safe_id_component
60 67
61 68
62 69 def FID(raw_id, path):
63 70 """
64 71 Creates a uniqe ID for filenode based on it's hash of path and revision
65 72 it's safe to use in urls
66 73
67 74 :param raw_id:
68 75 :param path:
69 76 """
70 77
71 78 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
72 79
73 80
74 81 def get_token():
75 82 """Return the current authentication token, creating one if one doesn't
76 83 already exist.
77 84 """
78 85 token_key = "_authentication_token"
79 86 from pylons import session
80 87 if not token_key in session:
81 88 try:
82 89 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
83 90 except AttributeError: # Python < 2.4
84 91 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
85 92 session[token_key] = token
86 93 if hasattr(session, 'save'):
87 94 session.save()
88 95 return session[token_key]
89 96
97
90 98 class _GetError(object):
91 99 """Get error from form_errors, and represent it as span wrapped error
92 100 message
93 101
94 102 :param field_name: field to fetch errors for
95 103 :param form_errors: form errors dict
96 104 """
97 105
98 106 def __call__(self, field_name, form_errors):
99 107 tmpl = """<span class="error_msg">%s</span>"""
100 108 if form_errors and form_errors.has_key(field_name):
101 109 return literal(tmpl % form_errors.get(field_name))
102 110
103 111 get_error = _GetError()
104 112
113
105 114 class _ToolTip(object):
106 115
107 116 def __call__(self, tooltip_title, trim_at=50):
108 117 """Special function just to wrap our text into nice formatted
109 118 autowrapped text
110 119
111 120 :param tooltip_title:
112 121 """
113 122 return escape(tooltip_title)
114 123 tooltip = _ToolTip()
115 124
125
116 126 class _FilesBreadCrumbs(object):
117 127
118 128 def __call__(self, repo_name, rev, paths):
119 129 if isinstance(paths, str):
120 130 paths = safe_unicode(paths)
121 131 url_l = [link_to(repo_name, url('files_home',
122 132 repo_name=repo_name,
123 133 revision=rev, f_path=''))]
124 134 paths_l = paths.split('/')
125 135 for cnt, p in enumerate(paths_l):
126 136 if p != '':
127 137 url_l.append(link_to(p,
128 138 url('files_home',
129 139 repo_name=repo_name,
130 140 revision=rev,
131 141 f_path='/'.join(paths_l[:cnt + 1])
132 142 )
133 143 )
134 144 )
135 145
136 146 return literal('/'.join(url_l))
137 147
138 148 files_breadcrumbs = _FilesBreadCrumbs()
139 149
150
140 151 class CodeHtmlFormatter(HtmlFormatter):
141 """My code Html Formatter for source codes
152 """
153 My code Html Formatter for source codes
142 154 """
143 155
144 156 def wrap(self, source, outfile):
145 157 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
146 158
147 159 def _wrap_code(self, source):
148 160 for cnt, it in enumerate(source):
149 161 i, t = it
150 162 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
151 163 yield i, t
152 164
153 165 def _wrap_tablelinenos(self, inner):
154 166 dummyoutfile = StringIO.StringIO()
155 167 lncount = 0
156 168 for t, line in inner:
157 169 if t:
158 170 lncount += 1
159 171 dummyoutfile.write(line)
160 172
161 173 fl = self.linenostart
162 174 mw = len(str(lncount + fl - 1))
163 175 sp = self.linenospecial
164 176 st = self.linenostep
165 177 la = self.lineanchors
166 178 aln = self.anchorlinenos
167 179 nocls = self.noclasses
168 180 if sp:
169 181 lines = []
170 182
171 183 for i in range(fl, fl + lncount):
172 184 if i % st == 0:
173 185 if i % sp == 0:
174 186 if aln:
175 187 lines.append('<a href="#%s%d" class="special">%*d</a>' %
176 188 (la, i, mw, i))
177 189 else:
178 190 lines.append('<span class="special">%*d</span>' % (mw, i))
179 191 else:
180 192 if aln:
181 193 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
182 194 else:
183 195 lines.append('%*d' % (mw, i))
184 196 else:
185 197 lines.append('')
186 198 ls = '\n'.join(lines)
187 199 else:
188 200 lines = []
189 201 for i in range(fl, fl + lncount):
190 202 if i % st == 0:
191 203 if aln:
192 204 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
193 205 else:
194 206 lines.append('%*d' % (mw, i))
195 207 else:
196 208 lines.append('')
197 209 ls = '\n'.join(lines)
198 210
199 211 # in case you wonder about the seemingly redundant <div> here: since the
200 212 # content in the other cell also is wrapped in a div, some browsers in
201 213 # some configurations seem to mess up the formatting...
202 214 if nocls:
203 215 yield 0, ('<table class="%stable">' % self.cssclass +
204 216 '<tr><td><div class="linenodiv" '
205 217 'style="background-color: #f0f0f0; padding-right: 10px">'
206 218 '<pre style="line-height: 125%">' +
207 219 ls + '</pre></div></td><td id="hlcode" class="code">')
208 220 else:
209 221 yield 0, ('<table class="%stable">' % self.cssclass +
210 222 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
211 223 ls + '</pre></div></td><td id="hlcode" class="code">')
212 224 yield 0, dummyoutfile.getvalue()
213 225 yield 0, '</td></tr></table>'
214 226
215 227
216 228 def pygmentize(filenode, **kwargs):
217 229 """pygmentize function using pygments
218 230
219 231 :param filenode:
220 232 """
221 233
222 234 return literal(code_highlight(filenode.content,
223 235 filenode.lexer, CodeHtmlFormatter(**kwargs)))
224 236
225 237
226 238 def pygmentize_annotation(repo_name, filenode, **kwargs):
227 239 """
228 240 pygmentize function for annotation
229 241
230 242 :param filenode:
231 243 """
232 244
233 245 color_dict = {}
234 246
235 247 def gen_color(n=10000):
236 248 """generator for getting n of evenly distributed colors using
237 249 hsv color and golden ratio. It always return same order of colors
238 250
239 251 :returns: RGB tuple
240 252 """
241 253
242 254 def hsv_to_rgb(h, s, v):
243 255 if s == 0.0:
244 256 return v, v, v
245 257 i = int(h * 6.0) # XXX assume int() truncates!
246 258 f = (h * 6.0) - i
247 259 p = v * (1.0 - s)
248 260 q = v * (1.0 - s * f)
249 261 t = v * (1.0 - s * (1.0 - f))
250 262 i = i % 6
251 263 if i == 0:
252 264 return v, t, p
253 265 if i == 1:
254 266 return q, v, p
255 267 if i == 2:
256 268 return p, v, t
257 269 if i == 3:
258 270 return p, q, v
259 271 if i == 4:
260 272 return t, p, v
261 273 if i == 5:
262 274 return v, p, q
263 275
264 276 golden_ratio = 0.618033988749895
265 277 h = 0.22717784590367374
266 278
267 279 for _ in xrange(n):
268 280 h += golden_ratio
269 281 h %= 1
270 282 HSV_tuple = [h, 0.95, 0.95]
271 283 RGB_tuple = hsv_to_rgb(*HSV_tuple)
272 284 yield map(lambda x: str(int(x * 256)), RGB_tuple)
273 285
274 286 cgenerator = gen_color()
275 287
276 288 def get_color_string(cs):
277 289 if cs in color_dict:
278 290 col = color_dict[cs]
279 291 else:
280 292 col = color_dict[cs] = cgenerator.next()
281 293 return "color: rgb(%s)! important;" % (', '.join(col))
282 294
283 295 def url_func(repo_name):
284 296
285 297 def _url_func(changeset):
286 298 author = changeset.author
287 299 date = changeset.date
288 300 message = tooltip(changeset.message)
289 301
290 302 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
291 303 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
292 304 "</b> %s<br/></div>")
293 305
294 306 tooltip_html = tooltip_html % (author, date, message)
295 307 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
296 308 short_id(changeset.raw_id))
297 309 uri = link_to(
298 310 lnk_format,
299 311 url('changeset_home', repo_name=repo_name,
300 312 revision=changeset.raw_id),
301 313 style=get_color_string(changeset.raw_id),
302 314 class_='tooltip',
303 315 title=tooltip_html
304 316 )
305 317
306 318 uri += '\n'
307 319 return uri
308 320 return _url_func
309 321
310 322 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
311 323
312 324
313 325 def is_following_repo(repo_name, user_id):
314 326 from rhodecode.model.scm import ScmModel
315 327 return ScmModel().is_following_repo(repo_name, user_id)
316 328
317 329 flash = _Flash()
318 330
319 331 #==============================================================================
320 332 # SCM FILTERS available via h.
321 333 #==============================================================================
322 334 from rhodecode.lib.vcs.utils import author_name, author_email
323 335 from rhodecode.lib.utils2 import credentials_filter, age as _age
324 336 from rhodecode.model.db import User
325 337
326 338 age = lambda x: _age(x)
327 339 capitalize = lambda x: x.capitalize()
328 340 email = author_email
329 341 short_id = lambda x: x[:12]
330 342 hide_credentials = lambda x: ''.join(credentials_filter(x))
331 343
332 344
333 345 def is_git(repository):
334 346 if hasattr(repository, 'alias'):
335 347 _type = repository.alias
336 348 elif hasattr(repository, 'repo_type'):
337 349 _type = repository.repo_type
338 350 else:
339 351 _type = repository
340 352 return _type == 'git'
341 353
342 354
343 355 def is_hg(repository):
344 356 if hasattr(repository, 'alias'):
345 357 _type = repository.alias
346 358 elif hasattr(repository, 'repo_type'):
347 359 _type = repository.repo_type
348 360 else:
349 361 _type = repository
350 362 return _type == 'hg'
351 363
352 364
353 365 def email_or_none(author):
354 366 _email = email(author)
355 367 if _email != '':
356 368 return _email
357 369
358 370 # See if it contains a username we can get an email from
359 371 user = User.get_by_username(author_name(author), case_insensitive=True,
360 372 cache=True)
361 373 if user is not None:
362 374 return user.email
363 375
364 376 # No valid email, not a valid user in the system, none!
365 377 return None
366 378
367 379
368 380 def person(author):
369 381 # attr to return from fetched user
370 382 person_getter = lambda usr: usr.username
371 383
372 384 # Valid email in the attribute passed, see if they're in the system
373 385 _email = email(author)
374 386 if _email != '':
375 387 user = User.get_by_email(_email, case_insensitive=True, cache=True)
376 388 if user is not None:
377 389 return person_getter(user)
378 390 return _email
379 391
380 392 # Maybe it's a username?
381 393 _author = author_name(author)
382 394 user = User.get_by_username(_author, case_insensitive=True,
383 395 cache=True)
384 396 if user is not None:
385 397 return person_getter(user)
386 398
387 399 # Still nothing? Just pass back the author name then
388 400 return _author
389 401
390 402
391 403 def bool2icon(value):
392 404 """Returns True/False values represented as small html image of true/false
393 405 icons
394 406
395 407 :param value: bool value
396 408 """
397 409
398 410 if value is True:
399 411 return HTML.tag('img', src=url("/images/icons/accept.png"),
400 412 alt=_('True'))
401 413
402 414 if value is False:
403 415 return HTML.tag('img', src=url("/images/icons/cancel.png"),
404 416 alt=_('False'))
405 417
406 418 return value
407 419
408 420
409 421 def action_parser(user_log, feed=False):
410 422 """
411 423 This helper will action_map the specified string action into translated
412 424 fancy names with icons and links
413 425
414 426 :param user_log: user log instance
415 427 :param feed: use output for feeds (no html and fancy icons)
416 428 """
417 429
418 430 action = user_log.action
419 431 action_params = ' '
420 432
421 433 x = action.split(':')
422 434
423 435 if len(x) > 1:
424 436 action, action_params = x
425 437
426 438 def get_cs_links():
427 439 revs_limit = 3 # display this amount always
428 440 revs_top_limit = 50 # show upto this amount of changesets hidden
429 441 revs_ids = action_params.split(',')
430 442 deleted = user_log.repository is None
431 443 if deleted:
432 444 return ','.join(revs_ids)
433 445
434 446 repo_name = user_log.repository.repo_name
435 447
436 448 repo = user_log.repository.scm_instance
437 449
438 450 message = lambda rev: rev.message
439 451 lnk = lambda rev, repo_name: (
440 452 link_to('r%s:%s' % (rev.revision, rev.short_id),
441 453 url('changeset_home', repo_name=repo_name,
442 454 revision=rev.raw_id),
443 455 title=tooltip(message(rev)), class_='tooltip')
444 456 )
445 457 # get only max revs_top_limit of changeset for performance/ui reasons
446 458 revs = [
447 459 x for x in repo.get_changesets(revs_ids[0],
448 460 revs_ids[:revs_top_limit][-1])
449 461 ]
450 462
451 463 cs_links = []
452 464 cs_links.append(" " + ', '.join(
453 465 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
454 466 )
455 467 )
456 468
457 469 compare_view = (
458 470 ' <div class="compare_view tooltip" title="%s">'
459 471 '<a href="%s">%s</a> </div>' % (
460 472 _('Show all combined changesets %s->%s') % (
461 473 revs_ids[0], revs_ids[-1]
462 474 ),
463 475 url('changeset_home', repo_name=repo_name,
464 476 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
465 477 ),
466 478 _('compare view')
467 479 )
468 480 )
469 481
470 482 # if we have exactly one more than normally displayed
471 483 # just display it, takes less space than displaying
472 484 # "and 1 more revisions"
473 485 if len(revs_ids) == revs_limit + 1:
474 486 rev = revs[revs_limit]
475 487 cs_links.append(", " + lnk(rev, repo_name))
476 488
477 489 # hidden-by-default ones
478 490 if len(revs_ids) > revs_limit + 1:
479 491 uniq_id = revs_ids[0]
480 492 html_tmpl = (
481 493 '<span> %s <a class="show_more" id="_%s" '
482 494 'href="#more">%s</a> %s</span>'
483 495 )
484 496 if not feed:
485 497 cs_links.append(html_tmpl % (
486 498 _('and'),
487 499 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
488 500 _('revisions')
489 501 )
490 502 )
491 503
492 504 if not feed:
493 505 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
494 506 else:
495 507 html_tmpl = '<span id="%s"> %s </span>'
496 508
497 509 morelinks = ', '.join(
498 510 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
499 511 )
500 512
501 513 if len(revs_ids) > revs_top_limit:
502 514 morelinks += ', ...'
503 515
504 516 cs_links.append(html_tmpl % (uniq_id, morelinks))
505 517 if len(revs) > 1:
506 518 cs_links.append(compare_view)
507 519 return ''.join(cs_links)
508 520
509 521 def get_fork_name():
510 522 repo_name = action_params
511 523 return _('fork name ') + str(link_to(action_params, url('summary_home',
512 524 repo_name=repo_name,)))
513 525
514 526 action_map = {'user_deleted_repo': (_('[deleted] repository'), None),
515 527 'user_created_repo': (_('[created] repository'), None),
516 528 'user_created_fork': (_('[created] repository as fork'), None),
517 529 'user_forked_repo': (_('[forked] repository'), get_fork_name),
518 530 'user_updated_repo': (_('[updated] repository'), None),
519 531 'admin_deleted_repo': (_('[delete] repository'), None),
520 532 'admin_created_repo': (_('[created] repository'), None),
521 533 'admin_forked_repo': (_('[forked] repository'), None),
522 534 'admin_updated_repo': (_('[updated] repository'), None),
523 535 'push': (_('[pushed] into'), get_cs_links),
524 536 'push_local': (_('[committed via RhodeCode] into'), get_cs_links),
525 537 'push_remote': (_('[pulled from remote] into'), get_cs_links),
526 538 'pull': (_('[pulled] from'), None),
527 539 'started_following_repo': (_('[started following] repository'), None),
528 540 'stopped_following_repo': (_('[stopped following] repository'), None),
529 541 }
530 542
531 543 action_str = action_map.get(action, action)
532 544 if feed:
533 545 action = action_str[0].replace('[', '').replace(']', '')
534 546 else:
535 547 action = action_str[0]\
536 548 .replace('[', '<span class="journal_highlight">')\
537 549 .replace(']', '</span>')
538 550
539 551 action_params_func = lambda: ""
540 552
541 553 if callable(action_str[1]):
542 554 action_params_func = action_str[1]
543 555
544 556 return [literal(action), action_params_func]
545 557
546 558
547 559 def action_parser_icon(user_log):
548 560 action = user_log.action
549 561 action_params = None
550 562 x = action.split(':')
551 563
552 564 if len(x) > 1:
553 565 action, action_params = x
554 566
555 567 tmpl = """<img src="%s%s" alt="%s"/>"""
556 568 map = {'user_deleted_repo':'database_delete.png',
557 569 'user_created_repo':'database_add.png',
558 570 'user_created_fork':'arrow_divide.png',
559 571 'user_forked_repo':'arrow_divide.png',
560 572 'user_updated_repo':'database_edit.png',
561 573 'admin_deleted_repo':'database_delete.png',
562 574 'admin_created_repo':'database_add.png',
563 575 'admin_forked_repo':'arrow_divide.png',
564 576 'admin_updated_repo':'database_edit.png',
565 577 'push':'script_add.png',
566 578 'push_local':'script_edit.png',
567 579 'push_remote':'connect.png',
568 580 'pull':'down_16.png',
569 581 'started_following_repo':'heart_add.png',
570 582 'stopped_following_repo':'heart_delete.png',
571 583 }
572 584 return literal(tmpl % ((url('/images/icons/')),
573 585 map.get(action, action), action))
574 586
575 587
576 588 #==============================================================================
577 589 # PERMS
578 590 #==============================================================================
579 591 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
580 592 HasRepoPermissionAny, HasRepoPermissionAll
581 593
582 594
583 595 #==============================================================================
584 596 # GRAVATAR URL
585 597 #==============================================================================
586 598
587 599 def gravatar_url(email_address, size=30):
588 600 if (not str2bool(config['app_conf'].get('use_gravatar')) or
589 601 not email_address or email_address == 'anonymous@rhodecode.org'):
590 602 f = lambda a, l: min(l, key=lambda x: abs(x - a))
591 603 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
592 604
593 605 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
594 606 default = 'identicon'
595 607 baseurl_nossl = "http://www.gravatar.com/avatar/"
596 608 baseurl_ssl = "https://secure.gravatar.com/avatar/"
597 609 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
598 610
599 611 if isinstance(email_address, unicode):
600 612 #hashlib crashes on unicode items
601 613 email_address = safe_str(email_address)
602 614 # construct the url
603 615 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
604 616 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
605 617
606 618 return gravatar_url
607 619
608 620
609 621 #==============================================================================
610 622 # REPO PAGER, PAGER FOR REPOSITORY
611 623 #==============================================================================
612 624 class RepoPage(Page):
613 625
614 626 def __init__(self, collection, page=1, items_per_page=20,
615 627 item_count=None, url=None, **kwargs):
616 628
617 629 """Create a "RepoPage" instance. special pager for paging
618 630 repository
619 631 """
620 632 self._url_generator = url
621 633
622 634 # Safe the kwargs class-wide so they can be used in the pager() method
623 635 self.kwargs = kwargs
624 636
625 637 # Save a reference to the collection
626 638 self.original_collection = collection
627 639
628 640 self.collection = collection
629 641
630 642 # The self.page is the number of the current page.
631 643 # The first page has the number 1!
632 644 try:
633 645 self.page = int(page) # make it int() if we get it as a string
634 646 except (ValueError, TypeError):
635 647 self.page = 1
636 648
637 649 self.items_per_page = items_per_page
638 650
639 651 # Unless the user tells us how many items the collections has
640 652 # we calculate that ourselves.
641 653 if item_count is not None:
642 654 self.item_count = item_count
643 655 else:
644 656 self.item_count = len(self.collection)
645 657
646 658 # Compute the number of the first and last available page
647 659 if self.item_count > 0:
648 660 self.first_page = 1
649 661 self.page_count = int(math.ceil(float(self.item_count) /
650 662 self.items_per_page))
651 663 self.last_page = self.first_page + self.page_count - 1
652 664
653 665 # Make sure that the requested page number is the range of
654 666 # valid pages
655 667 if self.page > self.last_page:
656 668 self.page = self.last_page
657 669 elif self.page < self.first_page:
658 670 self.page = self.first_page
659 671
660 672 # Note: the number of items on this page can be less than
661 673 # items_per_page if the last page is not full
662 674 self.first_item = max(0, (self.item_count) - (self.page *
663 675 items_per_page))
664 676 self.last_item = ((self.item_count - 1) - items_per_page *
665 677 (self.page - 1))
666 678
667 679 self.items = list(self.collection[self.first_item:self.last_item + 1])
668 680
669 681 # Links to previous and next page
670 682 if self.page > self.first_page:
671 683 self.previous_page = self.page - 1
672 684 else:
673 685 self.previous_page = None
674 686
675 687 if self.page < self.last_page:
676 688 self.next_page = self.page + 1
677 689 else:
678 690 self.next_page = None
679 691
680 692 # No items available
681 693 else:
682 694 self.first_page = None
683 695 self.page_count = 0
684 696 self.last_page = None
685 697 self.first_item = None
686 698 self.last_item = None
687 699 self.previous_page = None
688 700 self.next_page = None
689 701 self.items = []
690 702
691 703 # This is a subclass of the 'list' type. Initialise the list now.
692 704 list.__init__(self, reversed(self.items))
693 705
694 706
695 707 def changed_tooltip(nodes):
696 708 """
697 709 Generates a html string for changed nodes in changeset page.
698 710 It limits the output to 30 entries
699 711
700 712 :param nodes: LazyNodesGenerator
701 713 """
702 714 if nodes:
703 715 pref = ': <br/> '
704 716 suf = ''
705 717 if len(nodes) > 30:
706 718 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
707 719 return literal(pref + '<br/> '.join([safe_unicode(x.path)
708 720 for x in nodes[:30]]) + suf)
709 721 else:
710 722 return ': ' + _('No Files')
711 723
712 724
713 725 def repo_link(groups_and_repos):
714 726 """
715 727 Makes a breadcrumbs link to repo within a group
716 728 joins &raquo; on each group to create a fancy link
717 729
718 730 ex::
719 731 group >> subgroup >> repo
720 732
721 733 :param groups_and_repos:
722 734 """
723 735 groups, repo_name = groups_and_repos
724 736
725 737 if not groups:
726 738 return repo_name
727 739 else:
728 740 def make_link(group):
729 741 return link_to(group.name, url('repos_group_home',
730 742 group_name=group.group_name))
731 743 return literal(' &raquo; '.join(map(make_link, groups)) + \
732 744 " &raquo; " + repo_name)
733 745
734 746
735 747 def fancy_file_stats(stats):
736 748 """
737 749 Displays a fancy two colored bar for number of added/deleted
738 750 lines of code on file
739 751
740 752 :param stats: two element list of added/deleted lines of code
741 753 """
742 754
743 755 a, d, t = stats[0], stats[1], stats[0] + stats[1]
744 756 width = 100
745 757 unit = float(width) / (t or 1)
746 758
747 759 # needs > 9% of width to be visible or 0 to be hidden
748 760 a_p = max(9, unit * a) if a > 0 else 0
749 761 d_p = max(9, unit * d) if d > 0 else 0
750 762 p_sum = a_p + d_p
751 763
752 764 if p_sum > width:
753 765 #adjust the percentage to be == 100% since we adjusted to 9
754 766 if a_p > d_p:
755 767 a_p = a_p - (p_sum - width)
756 768 else:
757 769 d_p = d_p - (p_sum - width)
758 770
759 771 a_v = a if a > 0 else ''
760 772 d_v = d if d > 0 else ''
761 773
762 774 def cgen(l_type):
763 775 mapping = {'tr': 'top-right-rounded-corner-mid',
764 776 'tl': 'top-left-rounded-corner-mid',
765 777 'br': 'bottom-right-rounded-corner-mid',
766 778 'bl': 'bottom-left-rounded-corner-mid'}
767 779 map_getter = lambda x: mapping[x]
768 780
769 781 if l_type == 'a' and d_v:
770 782 #case when added and deleted are present
771 783 return ' '.join(map(map_getter, ['tl', 'bl']))
772 784
773 785 if l_type == 'a' and not d_v:
774 786 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
775 787
776 788 if l_type == 'd' and a_v:
777 789 return ' '.join(map(map_getter, ['tr', 'br']))
778 790
779 791 if l_type == 'd' and not a_v:
780 792 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
781 793
782 794 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
783 795 cgen('a'), a_p, a_v
784 796 )
785 797 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
786 798 cgen('d'), d_p, d_v
787 799 )
788 800 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
789 801
790 802
791 803 def urlify_text(text_):
792 804 import re
793 805
794 806 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
795 807 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
796 808
797 809 def url_func(match_obj):
798 810 url_full = match_obj.groups()[0]
799 811 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
800 812
801 813 return literal(url_pat.sub(url_func, text_))
802 814
803 815
804 816 def urlify_changesets(text_, repository):
805 817 """
806 818 Extract revision ids from changeset and make link from them
807 819
808 820 :param text_:
809 821 :param repository:
810 822 """
811 823 import re
812 824 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
813 825
814 826 def url_func(match_obj):
815 827 rev = match_obj.groups()[0]
816 828 pref = ''
817 829 if match_obj.group().startswith(' '):
818 830 pref = ' '
819 831 tmpl = (
820 832 '%(pref)s<a class="%(cls)s" href="%(url)s">'
821 833 '%(rev)s'
822 834 '</a>'
823 835 )
824 836 return tmpl % {
825 837 'pref': pref,
826 838 'cls': 'revision-link',
827 839 'url': url('changeset_home', repo_name=repository, revision=rev),
828 840 'rev': rev,
829 841 }
830 842
831 843 newtext = URL_PAT.sub(url_func, text_)
832 844
833 845 return newtext
834 846
835 847
836 848 def urlify_commit(text_, repository=None, link_=None):
837 849 """
838 850 Parses given text message and makes proper links.
839 851 issues are linked to given issue-server, and rest is a changeset link
840 852 if link_ is given, in other case it's a plain text
841 853
842 854 :param text_:
843 855 :param repository:
844 856 :param link_: changeset link
845 857 """
846 858 import re
847 859 import traceback
848 860
849 861 def escaper(string):
850 862 return string.replace('<', '&lt;').replace('>', '&gt;')
851 863
852 864 def linkify_others(t, l):
853 865 urls = re.compile(r'(\<a.*?\<\/a\>)',)
854 866 links = []
855 867 for e in urls.split(t):
856 868 if not urls.match(e):
857 869 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
858 870 else:
859 871 links.append(e)
860 872
861 873 return ''.join(links)
862 874
863 875
864 876 # urlify changesets - extrac revisions and make link out of them
865 877 text_ = urlify_changesets(escaper(text_), repository)
866 878
867 879 try:
868 880 conf = config['app_conf']
869 881
870 882 URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
871 883
872 884 if URL_PAT:
873 885 ISSUE_SERVER_LNK = conf.get('issue_server_link')
874 886 ISSUE_PREFIX = conf.get('issue_prefix')
875 887
876 888 def url_func(match_obj):
877 889 pref = ''
878 890 if match_obj.group().startswith(' '):
879 891 pref = ' '
880 892
881 893 issue_id = ''.join(match_obj.groups())
882 894 tmpl = (
883 895 '%(pref)s<a class="%(cls)s" href="%(url)s">'
884 896 '%(issue-prefix)s%(id-repr)s'
885 897 '</a>'
886 898 )
887 899 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
888 900 if repository:
889 901 url = url.replace('{repo}', repository)
890 902
891 903 return tmpl % {
892 904 'pref': pref,
893 905 'cls': 'issue-tracker-link',
894 906 'url': url,
895 907 'id-repr': issue_id,
896 908 'issue-prefix': ISSUE_PREFIX,
897 909 'serv': ISSUE_SERVER_LNK,
898 910 }
899 911
900 912 newtext = URL_PAT.sub(url_func, text_)
901 913
902 914 if link_:
903 915 # wrap not links into final link => link_
904 916 newtext = linkify_others(newtext, link_)
905 917
906 918 return literal(newtext)
907 919 except:
908 920 log.error(traceback.format_exc())
909 921 pass
910 922
911 923 return text_
912 924
913 925
914 926 def rst(source):
915 927 return literal('<div class="rst-block">%s</div>' %
916 928 MarkupRenderer.rst(source))
917 929
918 930
919 931 def rst_w_mentions(source):
920 932 """
921 933 Wrapped rst renderer with @mention highlighting
922 934
923 935 :param source:
924 936 """
925 937 return literal('<div class="rst-block">%s</div>' %
926 938 MarkupRenderer.rst_with_mentions(source))
@@ -1,230 +1,230 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(u'Home',h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <div class="table">
28 28 % if c.pagination:
29 29 <div id="graph">
30 30 <div id="graph_nodes">
31 31 <canvas id="graph_canvas"></canvas>
32 32 </div>
33 33 <div id="graph_content">
34 34 <div class="container_header">
35 35 ${h.form(h.url.current(),method='get')}
36 36 <div class="info_box" style="float:left">
37 37 ${h.submit('set',_('Show'),class_="ui-btn")}
38 38 ${h.text('size',size=1,value=c.size)}
39 39 ${_('revisions')}
40 40 </div>
41 41 ${h.end_form()}
42 42 <div id="rev_range_container" style="display:none"></div>
43 43 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
44 44 </div>
45 45
46 46 %for cnt,cs in enumerate(c.pagination):
47 47 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
48 48 <div class="left">
49 49 <div>
50 50 ${h.checkbox(cs.short_id,class_="changeset_range")}
51 51 <span class="tooltip" title="${h.age(cs.date)}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
52 52 </div>
53 53 <div class="author">
54 54 <div class="gravatar">
55 55 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
56 56 </div>
57 57 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
58 58 </div>
59 59 <div class="date">${cs.date}</div>
60 60 </div>
61 61 <div class="mid">
62 62 <div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
63 63 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
64 64 </div>
65 65 <div class="right">
66 66 <div id="${cs.raw_id}_changes_info" class="changes">
67 67 <div id="${cs.raw_id}" style="float:right;" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</div>
68 68 <div class="comments-container">
69 69 %if len(c.comments.get(cs.raw_id,[])) > 0:
70 70 <div class="comments-cnt" title="${('comments')}">
71 71 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
72 72 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
73 73 <img src="${h.url('/images/icons/comments.png')}">
74 74 </a>
75 75 </div>
76 76 %endif
77 77 </div>
78 78 </div>
79 79 %if cs.parents:
80 80 %for p_cs in reversed(cs.parents):
81 81 <div class="parent">${_('Parent')}
82 82 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
83 83 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
84 84 </div>
85 85 %endfor
86 86 %else:
87 87 <div class="parent">${_('No parents')}</div>
88 88 %endif
89 89
90 90 <span class="logtags">
91 91 %if len(cs.parents)>1:
92 92 <span class="merge">${_('merge')}</span>
93 93 %endif
94 94 %if h.is_hg(c.rhodecode_repo) and cs.branch:
95 95 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
96 ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
96 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
97 97 %endif
98 98 %for tag in cs.tags:
99 99 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
100 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
100 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
101 101 %endfor
102 102 </span>
103 103 </div>
104 104 </div>
105 105
106 106 %endfor
107 107 <div class="pagination-wh pagination-left">
108 108 ${c.pagination.pager('$link_previous ~2~ $link_next')}
109 109 </div>
110 110 </div>
111 111 </div>
112 112
113 113 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
114 114 <script type="text/javascript">
115 115 YAHOO.util.Event.onDOMReady(function(){
116 116
117 117 //Monitor range checkboxes and build a link to changesets
118 118 //ranges
119 119 var checkboxes = YUD.getElementsByClassName('changeset_range');
120 120 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
121 121 YUE.on(checkboxes,'click',function(e){
122 122 var checked_checkboxes = [];
123 123 for (pos in checkboxes){
124 124 if(checkboxes[pos].checked){
125 125 checked_checkboxes.push(checkboxes[pos]);
126 126 }
127 127 }
128 128 if(checked_checkboxes.length>1){
129 129 var rev_end = checked_checkboxes[0].name;
130 130 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
131 131
132 132 var url = url_tmpl.replace('__REVRANGE__',
133 133 rev_start+'...'+rev_end);
134 134
135 135 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
136 136 link = link.replace('__S',rev_start);
137 137 link = link.replace('__E',rev_end);
138 138 YUD.get('rev_range_container').innerHTML = link;
139 139 YUD.setStyle('rev_range_container','display','');
140 140 }
141 141 else{
142 142 YUD.setStyle('rev_range_container','display','none');
143 143
144 144 }
145 145 });
146 146
147 147 var msgs = YUQ('.message');
148 148 // get first element height
149 149 var el = YUQ('#graph_content .container')[0];
150 150 var row_h = el.clientHeight;
151 151 for(var i=0;i<msgs.length;i++){
152 152 var m = msgs[i];
153 153
154 154 var h = m.clientHeight;
155 155 var pad = YUD.getStyle(m,'padding');
156 156 if(h > row_h){
157 157 var offset = row_h - (h+12);
158 158 YUD.setStyle(m.nextElementSibling,'display','block');
159 159 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
160 160 };
161 161 }
162 162 YUE.on(YUQ('.expand'),'click',function(e){
163 163 var elem = e.currentTarget.parentNode.parentNode;
164 164 YUD.setStyle(e.currentTarget,'display','none');
165 165 YUD.setStyle(elem,'height','auto');
166 166
167 167 //redraw the graph, max_w and jsdata are global vars
168 168 set_canvas(max_w);
169 169
170 170 var r = new BranchRenderer();
171 171 r.render(jsdata,max_w);
172 172
173 173 })
174 174
175 175 // Fetch changeset details
176 176 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
177 177 var id = e.currentTarget.id
178 178 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
179 179 var url = url.replace('__CS__',id);
180 180 ypjax(url,id+'_changes_info',function(){tooltip_activate()});
181 181 });
182 182
183 183 // change branch filter
184 184 YUE.on(YUD.get('branch_filter'),'change',function(e){
185 185 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
186 186 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
187 187 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
188 188 var url = url.replace('__BRANCH__',selected_branch);
189 189 if(selected_branch != ''){
190 190 window.location = url;
191 191 }else{
192 192 window.location = url_main;
193 193 }
194 194
195 195 });
196 196
197 197 function set_canvas(heads) {
198 198 var c = document.getElementById('graph_nodes');
199 199 var t = document.getElementById('graph_content');
200 200 canvas = document.getElementById('graph_canvas');
201 201 var div_h = t.clientHeight;
202 202 c.style.height=div_h+'px';
203 203 canvas.setAttribute('height',div_h);
204 204 c.style.height=max_w+'px';
205 205 canvas.setAttribute('width',max_w);
206 206 };
207 207 var heads = 1;
208 208 var max_heads = 0;
209 209 var jsdata = ${c.jsdata|n};
210 210
211 211 for( var i=0;i<jsdata.length;i++){
212 212 var m = Math.max.apply(Math, jsdata[i][1]);
213 213 if (m>max_heads){
214 214 max_heads = m;
215 215 }
216 216 }
217 217 var max_w = Math.max(100,max_heads*25);
218 218 set_canvas(max_w);
219 219
220 220 var r = new BranchRenderer();
221 221 r.render(jsdata,max_w);
222 222
223 223 });
224 224 </script>
225 225 %else:
226 226 ${_('There are no changes yet')}
227 227 %endif
228 228 </div>
229 229 </div>
230 230 </%def>
General Comments 0
You need to be logged in to leave comments. Login now