##// END OF EJS Templates
merge beta into codereview
marcink -
r2280:c9e3ea5b merge codereview
parent child Browse files
Show More
1 NO CONTENT: new file 100644, binary diff hidden
This diff has been collapsed as it changes many lines, (2532 lines changed) Show them Hide them
@@ -0,0 +1,2532 b''
1 # Translations template for RhodeCode.
2 # Copyright (C) 2011 ORGANIZATION
3 # This file is distributed under the same license as the RhodeCode project.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
5 #
6 msgid ""
7 msgstr ""
8 "Project-Id-Version: RhodeCode 1.2.0\n"
9 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10 "POT-Creation-Date: 2011-09-14 15:50-0300\n"
11 "PO-Revision-Date: 2012-05-09 22:23+0800\n"
12 "Last-Translator: Nansen <nansenat16@gmail.com>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "MIME-Version: 1.0\n"
15 "Content-Type: text/plain; charset=utf-8\n"
16 "Content-Transfer-Encoding: 8bit\n"
17 "Generated-By: Babel 0.9.6\n"
18 "X-Poedit-Language: Chinese\n"
19 "X-Poedit-Country: TAIWAN\n"
20 "X-Poedit-SourceCharset: utf-8\n"
21
22 #: rhodecode/controllers/changeset.py:108
23 #: rhodecode/controllers/changeset.py:149
24 #: rhodecode/controllers/changeset.py:216
25 #: rhodecode/controllers/changeset.py:229
26 msgid "binary file"
27 msgstr "二進位檔"
28
29 #: rhodecode/controllers/changeset.py:123
30 #: rhodecode/controllers/changeset.py:168
31 msgid "Changeset is to big and was cut off, see raw changeset instead"
32 msgstr ""
33
34 #: rhodecode/controllers/changeset.py:159
35 msgid "Diff is to big and was cut off, see raw diff instead"
36 msgstr ""
37
38 #: rhodecode/controllers/error.py:69
39 msgid "Home page"
40 msgstr "首頁"
41
42 #: rhodecode/controllers/error.py:98
43 msgid "The request could not be understood by the server due to malformed syntax."
44 msgstr ""
45
46 #: rhodecode/controllers/error.py:101
47 msgid "Unauthorized access to resource"
48 msgstr ""
49
50 #: rhodecode/controllers/error.py:103
51 msgid "You don't have permission to view this page"
52 msgstr "您沒有權限瀏覽這個頁面"
53
54 #: rhodecode/controllers/error.py:105
55 msgid "The resource could not be found"
56 msgstr "找不到這個資源"
57
58 #: rhodecode/controllers/error.py:107
59 msgid "The server encountered an unexpected condition which prevented it from fulfilling the request."
60 msgstr ""
61
62 #: rhodecode/controllers/feed.py:48
63 #, python-format
64 msgid "Changes on %s repository"
65 msgstr "修改於版本庫 %s"
66
67 #: rhodecode/controllers/feed.py:49
68 #, python-format
69 msgid "%s %s feed"
70 msgstr ""
71
72 #: rhodecode/controllers/files.py:72
73 msgid "There are no files yet"
74 msgstr "尚未有任何檔案"
75
76 #: rhodecode/controllers/files.py:262
77 #, python-format
78 msgid "Edited %s via RhodeCode"
79 msgstr "使用 RhodeCode 編輯 %s"
80
81 #: rhodecode/controllers/files.py:267
82 #: rhodecode/templates/files/file_diff.html:40
83 msgid "No changes"
84 msgstr "沒有修改"
85
86 #: rhodecode/controllers/files.py:278
87 #, python-format
88 msgid "Successfully committed to %s"
89 msgstr "成功遞交至 %s"
90
91 #: rhodecode/controllers/files.py:283
92 msgid "Error occurred during commit"
93 msgstr ""
94
95 #: rhodecode/controllers/files.py:308
96 msgid "downloads disabled"
97 msgstr "下載已關閉"
98
99 #: rhodecode/controllers/files.py:313
100 #, python-format
101 msgid "Unknown revision %s"
102 msgstr "未知修訂 %s"
103
104 #: rhodecode/controllers/files.py:315
105 msgid "Empty repository"
106 msgstr "空的版本庫"
107
108 #: rhodecode/controllers/files.py:317
109 msgid "Unknown archive type"
110 msgstr "未知的存檔類型"
111
112 #: rhodecode/controllers/files.py:385
113 #: rhodecode/controllers/files.py:398
114 msgid "Binary file"
115 msgstr "二進位檔"
116
117 #: rhodecode/controllers/files.py:417
118 #: rhodecode/templates/changeset/changeset_range.html:4
119 #: rhodecode/templates/changeset/changeset_range.html:12
120 #: rhodecode/templates/changeset/changeset_range.html:29
121 msgid "Changesets"
122 msgstr "變更"
123
124 #: rhodecode/controllers/files.py:418
125 #: rhodecode/controllers/summary.py:175
126 #: rhodecode/templates/branches/branches.html:5
127 #: rhodecode/templates/summary/summary.html:690
128 msgid "Branches"
129 msgstr "分支"
130
131 #: rhodecode/controllers/files.py:419
132 #: rhodecode/controllers/summary.py:176
133 #: rhodecode/templates/summary/summary.html:679
134 #: rhodecode/templates/tags/tags.html:5
135 msgid "Tags"
136 msgstr "標籤"
137
138 #: rhodecode/controllers/journal.py:50
139 #, python-format
140 msgid "%s public journal %s feed"
141 msgstr "%s 公開日誌 %s feed"
142
143 #: rhodecode/controllers/journal.py:178
144 #: rhodecode/controllers/journal.py:212
145 #: rhodecode/templates/admin/repos/repo_edit.html:171
146 #: rhodecode/templates/base/base.html:50
147 msgid "Public journal"
148 msgstr "公開日誌"
149
150 #: rhodecode/controllers/login.py:111
151 msgid "You have successfully registered into rhodecode"
152 msgstr "您已經成功註冊rhodecode"
153
154 #: rhodecode/controllers/login.py:133
155 msgid "Your password reset link was sent"
156 msgstr "您的密碼重設連結已寄出"
157
158 #: rhodecode/controllers/login.py:155
159 msgid "Your password reset was successful, new password has been sent to your email"
160 msgstr "您的密碼重設動作已完成,新的密碼已寄至您的信箱"
161
162 #: rhodecode/controllers/search.py:109
163 msgid "Invalid search query. Try quoting it."
164 msgstr "無效的查詢。請使用跳脫字元"
165
166 #: rhodecode/controllers/search.py:114
167 msgid "There is no index to search in. Please run whoosh indexer"
168 msgstr "沒有任何索引可以搜尋。請執行 whoosh 建立索引"
169
170 #: rhodecode/controllers/search.py:118
171 msgid "An error occurred during this search operation"
172 msgstr ""
173
174 #: rhodecode/controllers/settings.py:61
175 #: rhodecode/controllers/settings.py:171
176 #, python-format
177 msgid "%s repository is not mapped to db perhaps it was created or renamed from the file system please run the application again in order to rescan repositories"
178 msgstr ""
179
180 #: rhodecode/controllers/settings.py:109
181 #: rhodecode/controllers/admin/repos.py:239
182 #, python-format
183 msgid "Repository %s updated successfully"
184 msgstr "版本庫 %s 更新完成"
185
186 #: rhodecode/controllers/settings.py:126
187 #: rhodecode/controllers/admin/repos.py:257
188 #, python-format
189 msgid "error occurred during update of repository %s"
190 msgstr ""
191
192 #: rhodecode/controllers/settings.py:144
193 #: rhodecode/controllers/admin/repos.py:275
194 #, python-format
195 msgid "%s repository is not mapped to db perhaps it was moved or renamed from the filesystem please run the application again in order to rescan repositories"
196 msgstr ""
197
198 #: rhodecode/controllers/settings.py:156
199 #: rhodecode/controllers/admin/repos.py:287
200 #, python-format
201 msgid "deleted repository %s"
202 msgstr "刪除版本庫 %s"
203
204 #: rhodecode/controllers/settings.py:159
205 #: rhodecode/controllers/admin/repos.py:297
206 #: rhodecode/controllers/admin/repos.py:303
207 #, python-format
208 msgid "An error occurred during deletion of %s"
209 msgstr ""
210
211 #: rhodecode/controllers/settings.py:193
212 #, python-format
213 msgid "forked %s repository as %s"
214 msgstr "forked %s 版本庫為 %s"
215
216 #: rhodecode/controllers/settings.py:211
217 #, python-format
218 msgid "An error occurred during repository forking %s"
219 msgstr ""
220
221 #: rhodecode/controllers/summary.py:123
222 msgid "No data loaded yet"
223 msgstr ""
224
225 #: rhodecode/controllers/summary.py:126
226 msgid "Statistics are disabled for this repository"
227 msgstr "這個版本庫的統計功能已停用"
228
229 #: rhodecode/controllers/admin/ldap_settings.py:49
230 msgid "BASE"
231 msgstr ""
232
233 #: rhodecode/controllers/admin/ldap_settings.py:50
234 msgid "ONELEVEL"
235 msgstr ""
236
237 #: rhodecode/controllers/admin/ldap_settings.py:51
238 msgid "SUBTREE"
239 msgstr ""
240
241 #: rhodecode/controllers/admin/ldap_settings.py:55
242 msgid "NEVER"
243 msgstr ""
244
245 #: rhodecode/controllers/admin/ldap_settings.py:56
246 msgid "ALLOW"
247 msgstr ""
248
249 #: rhodecode/controllers/admin/ldap_settings.py:57
250 msgid "TRY"
251 msgstr ""
252
253 #: rhodecode/controllers/admin/ldap_settings.py:58
254 msgid "DEMAND"
255 msgstr ""
256
257 #: rhodecode/controllers/admin/ldap_settings.py:59
258 msgid "HARD"
259 msgstr ""
260
261 #: rhodecode/controllers/admin/ldap_settings.py:63
262 msgid "No encryption"
263 msgstr "無加密"
264
265 #: rhodecode/controllers/admin/ldap_settings.py:64
266 msgid "LDAPS connection"
267 msgstr ""
268
269 #: rhodecode/controllers/admin/ldap_settings.py:65
270 msgid "START_TLS on LDAP connection"
271 msgstr ""
272
273 #: rhodecode/controllers/admin/ldap_settings.py:115
274 msgid "Ldap settings updated successfully"
275 msgstr "LDAP設定更新完成"
276
277 #: rhodecode/controllers/admin/ldap_settings.py:120
278 msgid "Unable to activate ldap. The \"python-ldap\" library is missing."
279 msgstr "無法啟用LDAP。找不到python-ldap函式庫"
280
281 #: rhodecode/controllers/admin/ldap_settings.py:134
282 msgid "error occurred during update of ldap settings"
283 msgstr ""
284
285 #: rhodecode/controllers/admin/permissions.py:56
286 msgid "None"
287 msgstr "無"
288
289 #: rhodecode/controllers/admin/permissions.py:57
290 msgid "Read"
291 msgstr "讀"
292
293 #: rhodecode/controllers/admin/permissions.py:58
294 msgid "Write"
295 msgstr "寫"
296
297 #: rhodecode/controllers/admin/permissions.py:59
298 #: rhodecode/templates/admin/ldap/ldap.html:9
299 #: rhodecode/templates/admin/permissions/permissions.html:9
300 #: rhodecode/templates/admin/repos/repo_add.html:9
301 #: rhodecode/templates/admin/repos/repo_edit.html:9
302 #: rhodecode/templates/admin/repos/repos.html:10
303 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:8
304 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:8
305 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:10
306 #: rhodecode/templates/admin/settings/hooks.html:9
307 #: rhodecode/templates/admin/settings/settings.html:9
308 #: rhodecode/templates/admin/users/user_add.html:8
309 #: rhodecode/templates/admin/users/user_edit.html:9
310 #: rhodecode/templates/admin/users/user_edit.html:110
311 #: rhodecode/templates/admin/users/users.html:9
312 #: rhodecode/templates/admin/users_groups/users_group_add.html:8
313 #: rhodecode/templates/admin/users_groups/users_group_edit.html:9
314 #: rhodecode/templates/admin/users_groups/users_groups.html:9
315 #: rhodecode/templates/base/base.html:279
316 #: rhodecode/templates/base/base.html:366
317 #: rhodecode/templates/base/base.html:368
318 #: rhodecode/templates/base/base.html:370
319 msgid "Admin"
320 msgstr "管理"
321
322 #: rhodecode/controllers/admin/permissions.py:62
323 msgid "disabled"
324 msgstr "停用"
325
326 #: rhodecode/controllers/admin/permissions.py:64
327 msgid "allowed with manual account activation"
328 msgstr "允許手動啟用帳號"
329
330 #: rhodecode/controllers/admin/permissions.py:66
331 msgid "allowed with automatic account activation"
332 msgstr "允許自動啟用帳號"
333
334 #: rhodecode/controllers/admin/permissions.py:68
335 msgid "Disabled"
336 msgstr "停用"
337
338 #: rhodecode/controllers/admin/permissions.py:69
339 msgid "Enabled"
340 msgstr "啟用"
341
342 #: rhodecode/controllers/admin/permissions.py:102
343 msgid "Default permissions updated successfully"
344 msgstr "預設權限更新完成"
345
346 #: rhodecode/controllers/admin/permissions.py:119
347 msgid "error occurred during update of permissions"
348 msgstr ""
349
350 #: rhodecode/controllers/admin/repos.py:96
351 #, python-format
352 msgid "%s repository is not mapped to db perhaps it was created or renamed from the filesystem please run the application again in order to rescan repositories"
353 msgstr ""
354
355 #: rhodecode/controllers/admin/repos.py:172
356 #, python-format
357 msgid "created repository %s from %s"
358 msgstr "建立版本庫 %s 到 %s"
359
360 #: rhodecode/controllers/admin/repos.py:176
361 #, python-format
362 msgid "created repository %s"
363 msgstr "建立版本庫 %s"
364
365 #: rhodecode/controllers/admin/repos.py:205
366 #, python-format
367 msgid "error occurred during creation of repository %s"
368 msgstr ""
369
370 #: rhodecode/controllers/admin/repos.py:292
371 #, python-format
372 msgid "Cannot delete %s it still contains attached forks"
373 msgstr ""
374
375 #: rhodecode/controllers/admin/repos.py:320
376 msgid "An error occurred during deletion of repository user"
377 msgstr ""
378
379 #: rhodecode/controllers/admin/repos.py:335
380 msgid "An error occurred during deletion of repository users groups"
381 msgstr ""
382
383 #: rhodecode/controllers/admin/repos.py:352
384 msgid "An error occurred during deletion of repository stats"
385 msgstr ""
386
387 #: rhodecode/controllers/admin/repos.py:367
388 msgid "An error occurred during cache invalidation"
389 msgstr ""
390
391 #: rhodecode/controllers/admin/repos.py:387
392 msgid "Updated repository visibility in public journal"
393 msgstr ""
394
395 #: rhodecode/controllers/admin/repos.py:390
396 msgid "An error occurred during setting this repository in public journal"
397 msgstr ""
398
399 #: rhodecode/controllers/admin/repos.py:395
400 #: rhodecode/model/forms.py:53
401 msgid "Token mismatch"
402 msgstr ""
403
404 #: rhodecode/controllers/admin/repos.py:408
405 msgid "Pulled from remote location"
406 msgstr ""
407
408 #: rhodecode/controllers/admin/repos.py:410
409 msgid "An error occurred during pull from remote location"
410 msgstr ""
411
412 #: rhodecode/controllers/admin/repos_groups.py:83
413 #, python-format
414 msgid "created repos group %s"
415 msgstr "建立版本庫群組 %s"
416
417 #: rhodecode/controllers/admin/repos_groups.py:96
418 #, python-format
419 msgid "error occurred during creation of repos group %s"
420 msgstr ""
421
422 #: rhodecode/controllers/admin/repos_groups.py:130
423 #, python-format
424 msgid "updated repos group %s"
425 msgstr "更新版本庫群組 %s"
426
427 #: rhodecode/controllers/admin/repos_groups.py:143
428 #, python-format
429 msgid "error occurred during update of repos group %s"
430 msgstr ""
431
432 #: rhodecode/controllers/admin/repos_groups.py:164
433 #, python-format
434 msgid "This group contains %s repositores and cannot be deleted"
435 msgstr ""
436
437 #: rhodecode/controllers/admin/repos_groups.py:171
438 #, python-format
439 msgid "removed repos group %s"
440 msgstr "移除版本庫群組 %s"
441
442 #: rhodecode/controllers/admin/repos_groups.py:175
443 #, python-format
444 msgid "error occurred during deletion of repos group %s"
445 msgstr ""
446
447 #: rhodecode/controllers/admin/settings.py:109
448 #, python-format
449 msgid "Repositories successfully rescanned added: %s,removed: %s"
450 msgstr ""
451
452 #: rhodecode/controllers/admin/settings.py:118
453 msgid "Whoosh reindex task scheduled"
454 msgstr "Whoosh 重新索引工作排程"
455
456 #: rhodecode/controllers/admin/settings.py:143
457 msgid "Updated application settings"
458 msgstr "更新應用設定"
459
460 #: rhodecode/controllers/admin/settings.py:148
461 #: rhodecode/controllers/admin/settings.py:215
462 msgid "error occurred during updating application settings"
463 msgstr ""
464
465 #: rhodecode/controllers/admin/settings.py:210
466 msgid "Updated mercurial settings"
467 msgstr "更新 mercurial 設定"
468
469 #: rhodecode/controllers/admin/settings.py:236
470 msgid "Added new hook"
471 msgstr "新增hook"
472
473 #: rhodecode/controllers/admin/settings.py:247
474 msgid "Updated hooks"
475 msgstr "更新hook"
476
477 #: rhodecode/controllers/admin/settings.py:251
478 msgid "error occurred during hook creation"
479 msgstr ""
480
481 #: rhodecode/controllers/admin/settings.py:310
482 msgid "You can't edit this user since it's crucial for entire application"
483 msgstr ""
484
485 #: rhodecode/controllers/admin/settings.py:339
486 msgid "Your account was updated successfully"
487 msgstr "您的帳號已更新完成"
488
489 #: rhodecode/controllers/admin/settings.py:359
490 #: rhodecode/controllers/admin/users.py:130
491 #, python-format
492 msgid "error occurred during update of user %s"
493 msgstr ""
494
495 #: rhodecode/controllers/admin/users.py:78
496 #, python-format
497 msgid "created user %s"
498 msgstr "建立使用者 %s"
499
500 #: rhodecode/controllers/admin/users.py:90
501 #, python-format
502 msgid "error occurred during creation of user %s"
503 msgstr ""
504
505 #: rhodecode/controllers/admin/users.py:116
506 msgid "User updated successfully"
507 msgstr "使用者更新完成"
508
509 #: rhodecode/controllers/admin/users.py:146
510 msgid "successfully deleted user"
511 msgstr "成功刪除使用者"
512
513 #: rhodecode/controllers/admin/users.py:150
514 msgid "An error occurred during deletion of user"
515 msgstr ""
516
517 #: rhodecode/controllers/admin/users.py:166
518 msgid "You can't edit this user"
519 msgstr "您無法編輯這位使用者"
520
521 #: rhodecode/controllers/admin/users.py:195
522 #: rhodecode/controllers/admin/users_groups.py:202
523 msgid "Granted 'repository create' permission to user"
524 msgstr ""
525
526 #: rhodecode/controllers/admin/users.py:204
527 #: rhodecode/controllers/admin/users_groups.py:211
528 msgid "Revoked 'repository create' permission to user"
529 msgstr ""
530
531 #: rhodecode/controllers/admin/users_groups.py:74
532 #, python-format
533 msgid "created users group %s"
534 msgstr "建立使用者群組 %s"
535
536 #: rhodecode/controllers/admin/users_groups.py:86
537 #, python-format
538 msgid "error occurred during creation of users group %s"
539 msgstr ""
540
541 #: rhodecode/controllers/admin/users_groups.py:119
542 #, python-format
543 msgid "updated users group %s"
544 msgstr "更新使用者群組 %s"
545
546 #: rhodecode/controllers/admin/users_groups.py:138
547 #, python-format
548 msgid "error occurred during update of users group %s"
549 msgstr ""
550
551 #: rhodecode/controllers/admin/users_groups.py:154
552 msgid "successfully deleted users group"
553 msgstr "成功移除使用者群組"
554
555 #: rhodecode/controllers/admin/users_groups.py:158
556 msgid "An error occurred during deletion of users group"
557 msgstr ""
558
559 #: rhodecode/lib/__init__.py:279
560 msgid "year"
561 msgstr "年"
562
563 #: rhodecode/lib/__init__.py:280
564 msgid "month"
565 msgstr "月"
566
567 #: rhodecode/lib/__init__.py:281
568 msgid "day"
569 msgstr "日"
570
571 #: rhodecode/lib/__init__.py:282
572 msgid "hour"
573 msgstr "時"
574
575 #: rhodecode/lib/__init__.py:283
576 msgid "minute"
577 msgstr "分"
578
579 #: rhodecode/lib/__init__.py:284
580 msgid "second"
581 msgstr "秒"
582
583 #: rhodecode/lib/__init__.py:293
584 msgid "ago"
585 msgstr "之前"
586
587 #: rhodecode/lib/__init__.py:296
588 msgid "just now"
589 msgstr "現在"
590
591 #: rhodecode/lib/auth.py:377
592 msgid "You need to be a registered user to perform this action"
593 msgstr "您必須是註冊使用者才能執行這個動作"
594
595 #: rhodecode/lib/auth.py:421
596 msgid "You need to be a signed in to view this page"
597 msgstr "您必須登入後才能瀏覽這個頁面"
598
599 #: rhodecode/lib/helpers.py:307
600 msgid "True"
601 msgstr "真"
602
603 #: rhodecode/lib/helpers.py:311
604 msgid "False"
605 msgstr "假"
606
607 #: rhodecode/lib/helpers.py:352
608 #, python-format
609 msgid "Show all combined changesets %s->%s"
610 msgstr ""
611
612 #: rhodecode/lib/helpers.py:356
613 msgid "compare view"
614 msgstr ""
615
616 #: rhodecode/lib/helpers.py:365
617 msgid "and"
618 msgstr "和"
619
620 #: rhodecode/lib/helpers.py:365
621 #, python-format
622 msgid "%s more"
623 msgstr ""
624
625 #: rhodecode/lib/helpers.py:367
626 #: rhodecode/templates/changelog/changelog.html:14
627 #: rhodecode/templates/changelog/changelog.html:39
628 msgid "revisions"
629 msgstr "修訂"
630
631 #: rhodecode/lib/helpers.py:385
632 msgid "fork name "
633 msgstr "fork 名稱"
634
635 #: rhodecode/lib/helpers.py:388
636 msgid "[deleted] repository"
637 msgstr ""
638
639 #: rhodecode/lib/helpers.py:389
640 #: rhodecode/lib/helpers.py:393
641 msgid "[created] repository"
642 msgstr ""
643
644 #: rhodecode/lib/helpers.py:390
645 #: rhodecode/lib/helpers.py:394
646 msgid "[forked] repository"
647 msgstr ""
648
649 #: rhodecode/lib/helpers.py:391
650 #: rhodecode/lib/helpers.py:395
651 msgid "[updated] repository"
652 msgstr ""
653
654 #: rhodecode/lib/helpers.py:392
655 msgid "[delete] repository"
656 msgstr ""
657
658 #: rhodecode/lib/helpers.py:396
659 msgid "[pushed] into"
660 msgstr ""
661
662 #: rhodecode/lib/helpers.py:397
663 msgid "[committed via RhodeCode] into"
664 msgstr ""
665
666 #: rhodecode/lib/helpers.py:398
667 msgid "[pulled from remote] into"
668 msgstr ""
669
670 #: rhodecode/lib/helpers.py:399
671 msgid "[pulled] from"
672 msgstr ""
673
674 #: rhodecode/lib/helpers.py:400
675 msgid "[started following] repository"
676 msgstr ""
677
678 #: rhodecode/lib/helpers.py:401
679 msgid "[stopped following] repository"
680 msgstr ""
681
682 #: rhodecode/lib/helpers.py:577
683 #, python-format
684 msgid " and %s more"
685 msgstr ""
686
687 #: rhodecode/lib/helpers.py:581
688 msgid "No Files"
689 msgstr "沒有檔案"
690
691 #: rhodecode/model/forms.py:66
692 msgid "Invalid username"
693 msgstr "無效的使用者名稱"
694
695 #: rhodecode/model/forms.py:75
696 msgid "This username already exists"
697 msgstr "使用者名稱已存在"
698
699 #: rhodecode/model/forms.py:79
700 msgid "Username may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character"
701 msgstr "使用者名稱只能使用字母數字、底線、小數點或破折號,且必須使用數字或字母開頭"
702
703 #: rhodecode/model/forms.py:94
704 msgid "Invalid group name"
705 msgstr "無效的群組名稱"
706
707 #: rhodecode/model/forms.py:104
708 msgid "This users group already exists"
709 msgstr "這個使用者群組已存在"
710
711 #: rhodecode/model/forms.py:110
712 msgid "Group name may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character"
713 msgstr "群組名稱只能使用字母數字、底線、小數點或破折號,且必須使用數字或字母開頭"
714
715 #: rhodecode/model/forms.py:132
716 msgid "Cannot assign this group as parent"
717 msgstr ""
718
719 #: rhodecode/model/forms.py:148
720 msgid "This group already exists"
721 msgstr "這個群組已存在"
722
723 #: rhodecode/model/forms.py:164
724 #: rhodecode/model/forms.py:172
725 #: rhodecode/model/forms.py:180
726 msgid "Invalid characters in password"
727 msgstr "無效的字元在密碼中"
728
729 #: rhodecode/model/forms.py:191
730 msgid "Passwords do not match"
731 msgstr "密碼不相符"
732
733 #: rhodecode/model/forms.py:196
734 msgid "invalid password"
735 msgstr "無效的密碼"
736
737 #: rhodecode/model/forms.py:197
738 msgid "invalid user name"
739 msgstr "無效的使用者名稱"
740
741 #: rhodecode/model/forms.py:198
742 msgid "Your account is disabled"
743 msgstr "您的帳號已被停用"
744
745 #: rhodecode/model/forms.py:233
746 msgid "This username is not valid"
747 msgstr "無效的使用者名稱"
748
749 #: rhodecode/model/forms.py:245
750 msgid "This repository name is disallowed"
751 msgstr "不允許的版本庫名稱"
752
753 #: rhodecode/model/forms.py:266
754 #, python-format
755 msgid "This repository already exists in group \"%s\""
756 msgstr "這個版本庫已存在於群組 \"%s\""
757
758 #: rhodecode/model/forms.py:274
759 msgid "This repository already exists"
760 msgstr "這個版本庫已經存在"
761
762 #: rhodecode/model/forms.py:312
763 #: rhodecode/model/forms.py:319
764 msgid "invalid clone url"
765 msgstr "無效的複製URL"
766
767 #: rhodecode/model/forms.py:322
768 msgid "Invalid clone url, provide a valid clone http\\s url"
769 msgstr ""
770
771 #: rhodecode/model/forms.py:334
772 msgid "Fork have to be the same type as original"
773 msgstr "Fork 必須使用相同的版本庫類型"
774
775 #: rhodecode/model/forms.py:341
776 msgid "This username or users group name is not valid"
777 msgstr "使用者名稱或群組名稱無效"
778
779 #: rhodecode/model/forms.py:403
780 msgid "This is not a valid path"
781 msgstr "不是一個有效的路徑"
782
783 #: rhodecode/model/forms.py:416
784 msgid "This e-mail address is already taken"
785 msgstr "這個郵件位址已經使用了"
786
787 #: rhodecode/model/forms.py:427
788 msgid "This e-mail address doesn't exist."
789 msgstr "這個郵件位址不存在"
790
791 #: rhodecode/model/forms.py:447
792 msgid "The LDAP Login attribute of the CN must be specified - this is the name of the attribute that is equivalent to 'username'"
793 msgstr ""
794
795 #: rhodecode/model/forms.py:466
796 msgid "Please enter a login"
797 msgstr "請登入"
798
799 #: rhodecode/model/forms.py:467
800 #, python-format
801 msgid "Enter a value %(min)i characters long or more"
802 msgstr ""
803
804 #: rhodecode/model/forms.py:475
805 msgid "Please enter a password"
806 msgstr "請輸入密碼"
807
808 #: rhodecode/model/forms.py:476
809 #, python-format
810 msgid "Enter %(min)i characters or more"
811 msgstr ""
812
813 #: rhodecode/model/user.py:145
814 msgid "[RhodeCode] New User registration"
815 msgstr "[RhodeCode] 新使用者註冊"
816
817 #: rhodecode/model/user.py:157
818 #: rhodecode/model/user.py:179
819 msgid "You can't Edit this user since it's crucial for entire application"
820 msgstr "您無法編輯這個使用者,因為他是系統帳號"
821
822 #: rhodecode/model/user.py:201
823 msgid "You can't remove this user since it's crucial for entire application"
824 msgstr "您無法移除這個使用者,因為他是系統帳號"
825
826 #: rhodecode/model/user.py:204
827 #, python-format
828 msgid "This user still owns %s repositories and cannot be removed. Switch owners or remove those repositories"
829 msgstr "這個使用者擁有 %s 個版本庫所以無法移除,請先變更版本庫擁有者或者刪除版本庫"
830
831 #: rhodecode/templates/index.html:4
832 msgid "Dashboard"
833 msgstr "儀表板"
834
835 #: rhodecode/templates/index_base.html:22
836 #: rhodecode/templates/admin/users/user_edit_my_account.html:102
837 msgid "quick filter..."
838 msgstr "快速過濾..."
839
840 #: rhodecode/templates/index_base.html:23
841 #: rhodecode/templates/base/base.html:300
842 msgid "repositories"
843 msgstr "個版本庫"
844
845 #: rhodecode/templates/index_base.html:29
846 #: rhodecode/templates/admin/repos/repos.html:22
847 msgid "ADD NEW REPOSITORY"
848 msgstr "新增版本庫"
849
850 #: rhodecode/templates/index_base.html:41
851 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:32
852 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:32
853 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:33
854 #: rhodecode/templates/admin/users_groups/users_group_add.html:32
855 #: rhodecode/templates/admin/users_groups/users_group_edit.html:33
856 msgid "Group name"
857 msgstr "群組名稱"
858
859 #: rhodecode/templates/index_base.html:42
860 #: rhodecode/templates/index_base.html:73
861 #: rhodecode/templates/admin/repos/repo_add_base.html:44
862 #: rhodecode/templates/admin/repos/repo_edit.html:64
863 #: rhodecode/templates/admin/repos/repos.html:31
864 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:41
865 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:41
866 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:34
867 #: rhodecode/templates/settings/repo_fork.html:40
868 #: rhodecode/templates/settings/repo_settings.html:40
869 #: rhodecode/templates/summary/summary.html:92
870 msgid "Description"
871 msgstr "描述"
872
873 #: rhodecode/templates/index_base.html:53
874 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:46
875 msgid "Repositories group"
876 msgstr "版本庫群組"
877
878 #: rhodecode/templates/index_base.html:72
879 #: rhodecode/templates/admin/repos/repo_add_base.html:9
880 #: rhodecode/templates/admin/repos/repo_edit.html:32
881 #: rhodecode/templates/admin/repos/repos.html:30
882 #: rhodecode/templates/admin/users/user_edit_my_account.html:117
883 #: rhodecode/templates/files/files_browser.html:157
884 #: rhodecode/templates/settings/repo_settings.html:31
885 #: rhodecode/templates/summary/summary.html:31
886 #: rhodecode/templates/summary/summary.html:107
887 msgid "Name"
888 msgstr "名稱"
889
890 #: rhodecode/templates/index_base.html:74
891 #: rhodecode/templates/admin/repos/repos.html:32
892 #: rhodecode/templates/summary/summary.html:114
893 msgid "Last change"
894 msgstr "最後修改"
895
896 #: rhodecode/templates/index_base.html:75
897 #: rhodecode/templates/admin/repos/repos.html:33
898 msgid "Tip"
899 msgstr ""
900
901 #: rhodecode/templates/index_base.html:76
902 #: rhodecode/templates/admin/repos/repo_edit.html:97
903 msgid "Owner"
904 msgstr "擁有者"
905
906 #: rhodecode/templates/index_base.html:77
907 #: rhodecode/templates/journal/public_journal.html:20
908 #: rhodecode/templates/summary/summary.html:180
909 #: rhodecode/templates/summary/summary.html:183
910 msgid "RSS"
911 msgstr ""
912
913 #: rhodecode/templates/index_base.html:78
914 #: rhodecode/templates/journal/public_journal.html:23
915 #: rhodecode/templates/summary/summary.html:181
916 #: rhodecode/templates/summary/summary.html:184
917 msgid "Atom"
918 msgstr ""
919
920 #: rhodecode/templates/index_base.html:87
921 #: rhodecode/templates/index_base.html:89
922 #: rhodecode/templates/index_base.html:91
923 #: rhodecode/templates/base/base.html:209
924 #: rhodecode/templates/base/base.html:211
925 #: rhodecode/templates/base/base.html:213
926 #: rhodecode/templates/summary/summary.html:4
927 msgid "Summary"
928 msgstr "概況"
929
930 #: rhodecode/templates/index_base.html:95
931 #: rhodecode/templates/index_base.html:97
932 #: rhodecode/templates/index_base.html:99
933 #: rhodecode/templates/base/base.html:225
934 #: rhodecode/templates/base/base.html:227
935 #: rhodecode/templates/base/base.html:229
936 #: rhodecode/templates/changelog/changelog.html:6
937 #: rhodecode/templates/changelog/changelog.html:14
938 msgid "Changelog"
939 msgstr "修改紀錄"
940
941 #: rhodecode/templates/index_base.html:103
942 #: rhodecode/templates/index_base.html:105
943 #: rhodecode/templates/index_base.html:107
944 #: rhodecode/templates/base/base.html:268
945 #: rhodecode/templates/base/base.html:270
946 #: rhodecode/templates/base/base.html:272
947 #: rhodecode/templates/files/files.html:4
948 msgid "Files"
949 msgstr "檔案"
950
951 #: rhodecode/templates/index_base.html:116
952 #: rhodecode/templates/admin/repos/repos.html:42
953 #: rhodecode/templates/admin/users/user_edit_my_account.html:127
954 #: rhodecode/templates/summary/summary.html:48
955 msgid "Mercurial repository"
956 msgstr "Mercurial 版本庫"
957
958 #: rhodecode/templates/index_base.html:118
959 #: rhodecode/templates/admin/repos/repos.html:44
960 #: rhodecode/templates/admin/users/user_edit_my_account.html:129
961 #: rhodecode/templates/summary/summary.html:51
962 msgid "Git repository"
963 msgstr "Git 版本庫"
964
965 #: rhodecode/templates/index_base.html:123
966 #: rhodecode/templates/admin/repos/repo_edit_perms.html:16
967 #: rhodecode/templates/journal/journal.html:53
968 #: rhodecode/templates/summary/summary.html:56
969 msgid "private repository"
970 msgstr "私有版本庫"
971
972 #: rhodecode/templates/index_base.html:125
973 #: rhodecode/templates/journal/journal.html:55
974 #: rhodecode/templates/summary/summary.html:58
975 msgid "public repository"
976 msgstr "公開版本庫"
977
978 #: rhodecode/templates/index_base.html:133
979 #: rhodecode/templates/base/base.html:291
980 #: rhodecode/templates/settings/repo_fork.html:13
981 msgid "fork"
982 msgstr ""
983
984 #: rhodecode/templates/index_base.html:134
985 #: rhodecode/templates/admin/repos/repos.html:60
986 #: rhodecode/templates/admin/users/user_edit_my_account.html:143
987 #: rhodecode/templates/summary/summary.html:69
988 #: rhodecode/templates/summary/summary.html:71
989 msgid "Fork of"
990 msgstr ""
991
992 #: rhodecode/templates/index_base.html:155
993 #: rhodecode/templates/admin/repos/repos.html:73
994 msgid "No changesets yet"
995 msgstr "尚未有任何變更"
996
997 #: rhodecode/templates/index_base.html:161
998 #: rhodecode/templates/index_base.html:163
999 #, python-format
1000 msgid "Subscribe to %s rss feed"
1001 msgstr "訂閱 %s rss"
1002
1003 #: rhodecode/templates/index_base.html:168
1004 #: rhodecode/templates/index_base.html:170
1005 #, python-format
1006 msgid "Subscribe to %s atom feed"
1007 msgstr "訂閱 %s atom"
1008
1009 #: rhodecode/templates/login.html:5
1010 #: rhodecode/templates/login.html:54
1011 #: rhodecode/templates/base/base.html:38
1012 msgid "Sign In"
1013 msgstr "登入"
1014
1015 #: rhodecode/templates/login.html:21
1016 msgid "Sign In to"
1017 msgstr "登入"
1018
1019 #: rhodecode/templates/login.html:31
1020 #: rhodecode/templates/register.html:20
1021 #: rhodecode/templates/admin/admin_log.html:5
1022 #: rhodecode/templates/admin/users/user_add.html:32
1023 #: rhodecode/templates/admin/users/user_edit.html:47
1024 #: rhodecode/templates/admin/users/user_edit_my_account.html:45
1025 #: rhodecode/templates/base/base.html:15
1026 #: rhodecode/templates/summary/summary.html:106
1027 msgid "Username"
1028 msgstr "帳號"
1029
1030 #: rhodecode/templates/login.html:40
1031 #: rhodecode/templates/register.html:29
1032 #: rhodecode/templates/admin/ldap/ldap.html:46
1033 #: rhodecode/templates/admin/users/user_add.html:41
1034 #: rhodecode/templates/base/base.html:24
1035 msgid "Password"
1036 msgstr "密碼"
1037
1038 #: rhodecode/templates/login.html:60
1039 msgid "Forgot your password ?"
1040 msgstr "忘記您的密碼?"
1041
1042 #: rhodecode/templates/login.html:63
1043 #: rhodecode/templates/base/base.html:35
1044 msgid "Don't have an account ?"
1045 msgstr "沒有帳號?"
1046
1047 #: rhodecode/templates/password_reset.html:5
1048 msgid "Reset your password"
1049 msgstr "重設您的密碼"
1050
1051 #: rhodecode/templates/password_reset.html:11
1052 msgid "Reset your password to"
1053 msgstr "重設您的密碼"
1054
1055 #: rhodecode/templates/password_reset.html:21
1056 msgid "Email address"
1057 msgstr "郵件位址"
1058
1059 #: rhodecode/templates/password_reset.html:30
1060 msgid "Reset my password"
1061 msgstr "重設我的密碼"
1062
1063 #: rhodecode/templates/password_reset.html:31
1064 msgid "Password reset link will be send to matching email address"
1065 msgstr "密碼重設連結已郵寄至您的信箱"
1066
1067 #: rhodecode/templates/register.html:5
1068 #: rhodecode/templates/register.html:74
1069 msgid "Sign Up"
1070 msgstr "登入"
1071
1072 #: rhodecode/templates/register.html:11
1073 msgid "Sign Up to"
1074 msgstr "登入"
1075
1076 #: rhodecode/templates/register.html:38
1077 msgid "Re-enter password"
1078 msgstr "確認密碼"
1079
1080 #: rhodecode/templates/register.html:47
1081 #: rhodecode/templates/admin/users/user_add.html:50
1082 #: rhodecode/templates/admin/users/user_edit.html:74
1083 #: rhodecode/templates/admin/users/user_edit_my_account.html:63
1084 msgid "First Name"
1085 msgstr "名"
1086
1087 #: rhodecode/templates/register.html:56
1088 #: rhodecode/templates/admin/users/user_add.html:59
1089 #: rhodecode/templates/admin/users/user_edit.html:83
1090 #: rhodecode/templates/admin/users/user_edit_my_account.html:72
1091 msgid "Last Name"
1092 msgstr "姓"
1093
1094 #: rhodecode/templates/register.html:65
1095 #: rhodecode/templates/admin/users/user_add.html:68
1096 #: rhodecode/templates/admin/users/user_edit.html:92
1097 #: rhodecode/templates/admin/users/user_edit_my_account.html:81
1098 #: rhodecode/templates/summary/summary.html:108
1099 msgid "Email"
1100 msgstr "電子郵件"
1101
1102 #: rhodecode/templates/register.html:76
1103 msgid "Your account will be activated right after registration"
1104 msgstr "您的帳號註冊後將會啟用"
1105
1106 #: rhodecode/templates/register.html:78
1107 msgid "Your account must wait for activation by administrator"
1108 msgstr "您的帳號註冊後將等待管理員啟用"
1109
1110 #: rhodecode/templates/repo_switcher_list.html:14
1111 msgid "Private repository"
1112 msgstr "私有的版本庫"
1113
1114 #: rhodecode/templates/repo_switcher_list.html:19
1115 msgid "Public repository"
1116 msgstr "公開的版本庫"
1117
1118 #: rhodecode/templates/admin/admin.html:5
1119 #: rhodecode/templates/admin/admin.html:9
1120 msgid "Admin journal"
1121 msgstr "管理員日誌"
1122
1123 #: rhodecode/templates/admin/admin_log.html:6
1124 msgid "Action"
1125 msgstr "動作"
1126
1127 #: rhodecode/templates/admin/admin_log.html:7
1128 msgid "Repository"
1129 msgstr "版本庫"
1130
1131 #: rhodecode/templates/admin/admin_log.html:8
1132 msgid "Date"
1133 msgstr "時間"
1134
1135 #: rhodecode/templates/admin/admin_log.html:9
1136 msgid "From IP"
1137 msgstr "來源IP"
1138
1139 #: rhodecode/templates/admin/admin_log.html:52
1140 msgid "No actions yet"
1141 msgstr ""
1142
1143 #: rhodecode/templates/admin/ldap/ldap.html:5
1144 msgid "LDAP administration"
1145 msgstr "LDAP管理者"
1146
1147 #: rhodecode/templates/admin/ldap/ldap.html:11
1148 msgid "Ldap"
1149 msgstr ""
1150
1151 #: rhodecode/templates/admin/ldap/ldap.html:28
1152 msgid "Connection settings"
1153 msgstr "連接設定"
1154
1155 #: rhodecode/templates/admin/ldap/ldap.html:30
1156 msgid "Enable LDAP"
1157 msgstr "啟動LDAP"
1158
1159 #: rhodecode/templates/admin/ldap/ldap.html:34
1160 msgid "Host"
1161 msgstr "主機"
1162
1163 #: rhodecode/templates/admin/ldap/ldap.html:38
1164 msgid "Port"
1165 msgstr "連接埠"
1166
1167 #: rhodecode/templates/admin/ldap/ldap.html:42
1168 msgid "Account"
1169 msgstr "帳號"
1170
1171 #: rhodecode/templates/admin/ldap/ldap.html:50
1172 msgid "Connection security"
1173 msgstr "連接安全性"
1174
1175 #: rhodecode/templates/admin/ldap/ldap.html:54
1176 msgid "Certificate Checks"
1177 msgstr "憑證確認"
1178
1179 #: rhodecode/templates/admin/ldap/ldap.html:57
1180 msgid "Search settings"
1181 msgstr "搜尋選項"
1182
1183 #: rhodecode/templates/admin/ldap/ldap.html:59
1184 msgid "Base DN"
1185 msgstr ""
1186
1187 #: rhodecode/templates/admin/ldap/ldap.html:63
1188 msgid "LDAP Filter"
1189 msgstr ""
1190
1191 #: rhodecode/templates/admin/ldap/ldap.html:67
1192 msgid "LDAP Search Scope"
1193 msgstr ""
1194
1195 #: rhodecode/templates/admin/ldap/ldap.html:70
1196 msgid "Attribute mappings"
1197 msgstr "屬性對應"
1198
1199 #: rhodecode/templates/admin/ldap/ldap.html:72
1200 msgid "Login Attribute"
1201 msgstr "登入屬性"
1202
1203 #: rhodecode/templates/admin/ldap/ldap.html:76
1204 msgid "First Name Attribute"
1205 msgstr "名"
1206
1207 #: rhodecode/templates/admin/ldap/ldap.html:80
1208 msgid "Last Name Attribute"
1209 msgstr "姓"
1210
1211 #: rhodecode/templates/admin/ldap/ldap.html:84
1212 msgid "E-mail Attribute"
1213 msgstr "電子郵件屬性"
1214
1215 #: rhodecode/templates/admin/ldap/ldap.html:89
1216 #: rhodecode/templates/admin/settings/hooks.html:73
1217 #: rhodecode/templates/admin/users/user_edit.html:117
1218 #: rhodecode/templates/admin/users/user_edit.html:142
1219 #: rhodecode/templates/admin/users/user_edit_my_account.html:89
1220 #: rhodecode/templates/admin/users_groups/users_group_edit.html:263
1221 msgid "Save"
1222 msgstr "儲存"
1223
1224 #: rhodecode/templates/admin/permissions/permissions.html:5
1225 msgid "Permissions administration"
1226 msgstr "權限管理員"
1227
1228 #: rhodecode/templates/admin/permissions/permissions.html:11
1229 #: rhodecode/templates/admin/repos/repo_edit.html:109
1230 #: rhodecode/templates/admin/users/user_edit.html:127
1231 #: rhodecode/templates/admin/users_groups/users_group_edit.html:248
1232 #: rhodecode/templates/settings/repo_settings.html:58
1233 msgid "Permissions"
1234 msgstr "權限"
1235
1236 #: rhodecode/templates/admin/permissions/permissions.html:24
1237 msgid "Default permissions"
1238 msgstr "預設權限"
1239
1240 #: rhodecode/templates/admin/permissions/permissions.html:31
1241 msgid "Anonymous access"
1242 msgstr "訪客權限"
1243
1244 #: rhodecode/templates/admin/permissions/permissions.html:41
1245 msgid "Repository permission"
1246 msgstr "版本庫權限"
1247
1248 #: rhodecode/templates/admin/permissions/permissions.html:49
1249 msgid "All default permissions on each repository will be reset to choosen permission, note that all custom default permission on repositories will be lost"
1250 msgstr ""
1251
1252 #: rhodecode/templates/admin/permissions/permissions.html:50
1253 msgid "overwrite existing settings"
1254 msgstr "複寫已存在設定"
1255
1256 #: rhodecode/templates/admin/permissions/permissions.html:55
1257 msgid "Registration"
1258 msgstr "註冊"
1259
1260 #: rhodecode/templates/admin/permissions/permissions.html:63
1261 msgid "Repository creation"
1262 msgstr "版本庫建立"
1263
1264 #: rhodecode/templates/admin/permissions/permissions.html:71
1265 msgid "set"
1266 msgstr "設定"
1267
1268 #: rhodecode/templates/admin/repos/repo_add.html:5
1269 #: rhodecode/templates/admin/repos/repo_add_create_repository.html:5
1270 msgid "Add repository"
1271 msgstr "新增版本庫"
1272
1273 #: rhodecode/templates/admin/repos/repo_add.html:11
1274 #: rhodecode/templates/admin/repos/repo_edit.html:11
1275 #: rhodecode/templates/admin/repos/repos.html:10
1276 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:10
1277 msgid "Repositories"
1278 msgstr "版本庫"
1279
1280 #: rhodecode/templates/admin/repos/repo_add.html:13
1281 msgid "add new"
1282 msgstr "新增"
1283
1284 #: rhodecode/templates/admin/repos/repo_add_base.html:20
1285 #: rhodecode/templates/summary/summary.html:80
1286 #: rhodecode/templates/summary/summary.html:82
1287 msgid "Clone from"
1288 msgstr "複製由"
1289
1290 #: rhodecode/templates/admin/repos/repo_add_base.html:28
1291 #: rhodecode/templates/admin/repos/repo_edit.html:48
1292 #: rhodecode/templates/admin/repos_groups/repos_groups.html:4
1293 msgid "Repository group"
1294 msgstr "版本庫群組"
1295
1296 #: rhodecode/templates/admin/repos/repo_add_base.html:36
1297 #: rhodecode/templates/admin/repos/repo_edit.html:56
1298 msgid "Type"
1299 msgstr "類型"
1300
1301 #: rhodecode/templates/admin/repos/repo_add_base.html:52
1302 #: rhodecode/templates/admin/repos/repo_edit.html:73
1303 #: rhodecode/templates/settings/repo_fork.html:48
1304 #: rhodecode/templates/settings/repo_settings.html:49
1305 msgid "Private"
1306 msgstr "私有"
1307
1308 #: rhodecode/templates/admin/repos/repo_add_base.html:59
1309 msgid "add"
1310 msgstr "新增"
1311
1312 #: rhodecode/templates/admin/repos/repo_add_create_repository.html:9
1313 msgid "add new repository"
1314 msgstr "新增版本庫"
1315
1316 #: rhodecode/templates/admin/repos/repo_edit.html:5
1317 msgid "Edit repository"
1318 msgstr "編輯版本庫"
1319
1320 #: rhodecode/templates/admin/repos/repo_edit.html:13
1321 #: rhodecode/templates/admin/users/user_edit.html:13
1322 #: rhodecode/templates/admin/users/user_edit_my_account.html:148
1323 #: rhodecode/templates/admin/users_groups/users_group_edit.html:13
1324 #: rhodecode/templates/files/files_annotate.html:49
1325 #: rhodecode/templates/files/files_source.html:20
1326 msgid "edit"
1327 msgstr "編輯"
1328
1329 #: rhodecode/templates/admin/repos/repo_edit.html:40
1330 msgid "Clone uri"
1331 msgstr "複製URL"
1332
1333 #: rhodecode/templates/admin/repos/repo_edit.html:81
1334 msgid "Enable statistics"
1335 msgstr "啟用統計"
1336
1337 #: rhodecode/templates/admin/repos/repo_edit.html:89
1338 msgid "Enable downloads"
1339 msgstr "啟用下載"
1340
1341 #: rhodecode/templates/admin/repos/repo_edit.html:127
1342 msgid "Administration"
1343 msgstr "管理者"
1344
1345 #: rhodecode/templates/admin/repos/repo_edit.html:130
1346 msgid "Statistics"
1347 msgstr "統計"
1348
1349 #: rhodecode/templates/admin/repos/repo_edit.html:134
1350 msgid "Reset current statistics"
1351 msgstr "重設目前的統計"
1352
1353 #: rhodecode/templates/admin/repos/repo_edit.html:134
1354 msgid "Confirm to remove current statistics"
1355 msgstr "確認移除目前的統計"
1356
1357 #: rhodecode/templates/admin/repos/repo_edit.html:137
1358 msgid "Fetched to rev"
1359 msgstr ""
1360
1361 #: rhodecode/templates/admin/repos/repo_edit.html:138
1362 msgid "Percentage of stats gathered"
1363 msgstr ""
1364
1365 #: rhodecode/templates/admin/repos/repo_edit.html:147
1366 msgid "Remote"
1367 msgstr "遠端"
1368
1369 #: rhodecode/templates/admin/repos/repo_edit.html:151
1370 msgid "Pull changes from remote location"
1371 msgstr ""
1372
1373 #: rhodecode/templates/admin/repos/repo_edit.html:151
1374 msgid "Confirm to pull changes from remote side"
1375 msgstr ""
1376
1377 #: rhodecode/templates/admin/repos/repo_edit.html:162
1378 msgid "Cache"
1379 msgstr "快取"
1380
1381 #: rhodecode/templates/admin/repos/repo_edit.html:166
1382 msgid "Invalidate repository cache"
1383 msgstr ""
1384
1385 #: rhodecode/templates/admin/repos/repo_edit.html:166
1386 msgid "Confirm to invalidate repository cache"
1387 msgstr "確認廢止版本庫快取"
1388
1389 #: rhodecode/templates/admin/repos/repo_edit.html:177
1390 msgid "Remove from public journal"
1391 msgstr "從公開日誌移除"
1392
1393 #: rhodecode/templates/admin/repos/repo_edit.html:179
1394 msgid "Add to public journal"
1395 msgstr "新增至公開日誌"
1396
1397 #: rhodecode/templates/admin/repos/repo_edit.html:185
1398 msgid "Delete"
1399 msgstr "移除"
1400
1401 #: rhodecode/templates/admin/repos/repo_edit.html:189
1402 msgid "Remove this repository"
1403 msgstr "移除版本庫"
1404
1405 #: rhodecode/templates/admin/repos/repo_edit.html:189
1406 #: rhodecode/templates/admin/repos/repos.html:79
1407 msgid "Confirm to delete this repository"
1408 msgstr "確認移除這個版本庫"
1409
1410 #: rhodecode/templates/admin/repos/repo_edit_perms.html:3
1411 msgid "none"
1412 msgstr "無"
1413
1414 #: rhodecode/templates/admin/repos/repo_edit_perms.html:4
1415 msgid "read"
1416 msgstr "讀"
1417
1418 #: rhodecode/templates/admin/repos/repo_edit_perms.html:5
1419 msgid "write"
1420 msgstr "寫"
1421
1422 #: rhodecode/templates/admin/repos/repo_edit_perms.html:6
1423 #: rhodecode/templates/admin/users/users.html:38
1424 #: rhodecode/templates/base/base.html:296
1425 msgid "admin"
1426 msgstr "管理員"
1427
1428 #: rhodecode/templates/admin/repos/repo_edit_perms.html:7
1429 msgid "member"
1430 msgstr "成員"
1431
1432 #: rhodecode/templates/admin/repos/repo_edit_perms.html:33
1433 #: rhodecode/templates/admin/repos/repo_edit_perms.html:53
1434 msgid "revoke"
1435 msgstr ""
1436
1437 #: rhodecode/templates/admin/repos/repo_edit_perms.html:75
1438 msgid "Add another member"
1439 msgstr "新增另ㄧ位成員"
1440
1441 #: rhodecode/templates/admin/repos/repo_edit_perms.html:89
1442 msgid "Failed to remove user"
1443 msgstr "移除使用者失敗"
1444
1445 #: rhodecode/templates/admin/repos/repo_edit_perms.html:104
1446 msgid "Failed to remove users group"
1447 msgstr "移除使用者群組失敗"
1448
1449 #: rhodecode/templates/admin/repos/repo_edit_perms.html:205
1450 msgid "Group"
1451 msgstr "群組"
1452
1453 #: rhodecode/templates/admin/repos/repo_edit_perms.html:206
1454 #: rhodecode/templates/admin/users_groups/users_groups.html:33
1455 msgid "members"
1456 msgstr "成員"
1457
1458 #: rhodecode/templates/admin/repos/repos.html:5
1459 msgid "Repositories administration"
1460 msgstr "版本庫管理員"
1461
1462 #: rhodecode/templates/admin/repos/repos.html:34
1463 #: rhodecode/templates/summary/summary.html:100
1464 msgid "Contact"
1465 msgstr "聯絡方式"
1466
1467 #: rhodecode/templates/admin/repos/repos.html:35
1468 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:36
1469 #: rhodecode/templates/admin/users/user_edit_my_account.html:119
1470 #: rhodecode/templates/admin/users/users.html:40
1471 #: rhodecode/templates/admin/users_groups/users_groups.html:35
1472 msgid "action"
1473 msgstr "動作"
1474
1475 #: rhodecode/templates/admin/repos/repos.html:51
1476 #: rhodecode/templates/admin/users/user_edit_my_account.html:134
1477 #: rhodecode/templates/admin/users/user_edit_my_account.html:148
1478 msgid "private"
1479 msgstr "私有"
1480
1481 #: rhodecode/templates/admin/repos/repos.html:53
1482 #: rhodecode/templates/admin/repos/repos.html:59
1483 #: rhodecode/templates/admin/users/user_edit_my_account.html:136
1484 #: rhodecode/templates/admin/users/user_edit_my_account.html:142
1485 #: rhodecode/templates/summary/summary.html:68
1486 msgid "public"
1487 msgstr "公開"
1488
1489 #: rhodecode/templates/admin/repos/repos.html:79
1490 #: rhodecode/templates/admin/users/users.html:55
1491 msgid "delete"
1492 msgstr "刪除"
1493
1494 #: rhodecode/templates/admin/repos_groups/repos_groups.html:8
1495 msgid "Groups"
1496 msgstr "群組"
1497
1498 #: rhodecode/templates/admin/repos_groups/repos_groups.html:13
1499 msgid "with"
1500 msgstr ""
1501
1502 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:5
1503 msgid "Add repos group"
1504 msgstr "新增版本庫群組"
1505
1506 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:10
1507 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:10
1508 msgid "Repos groups"
1509 msgstr "版本庫群組"
1510
1511 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:12
1512 msgid "add new repos group"
1513 msgstr "新增版本庫群組"
1514
1515 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:50
1516 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:50
1517 msgid "Group parent"
1518 msgstr "父群組"
1519
1520 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:58
1521 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:58
1522 #: rhodecode/templates/admin/users/user_add.html:85
1523 #: rhodecode/templates/admin/users_groups/users_group_add.html:49
1524 #: rhodecode/templates/admin/users_groups/users_group_edit.html:90
1525 msgid "save"
1526 msgstr "儲存"
1527
1528 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:5
1529 msgid "Edit repos group"
1530 msgstr "編輯版本庫群組"
1531
1532 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:12
1533 msgid "edit repos group"
1534 msgstr "編輯版本庫群組"
1535
1536 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:5
1537 msgid "Repositories groups administration"
1538 msgstr "版本庫群組管理員"
1539
1540 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:22
1541 msgid "ADD NEW GROUP"
1542 msgstr "新增群組"
1543
1544 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:35
1545 msgid "Number of repositories"
1546 msgstr "版本庫數量"
1547
1548 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:54
1549 msgid "Confirm to delete this group"
1550 msgstr "確認刪除這個群組"
1551
1552 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:62
1553 msgid "There are no repositories groups yet"
1554 msgstr "沒有任何版本庫群組"
1555
1556 #: rhodecode/templates/admin/settings/hooks.html:5
1557 #: rhodecode/templates/admin/settings/settings.html:5
1558 msgid "Settings administration"
1559 msgstr "設定管理員"
1560
1561 #: rhodecode/templates/admin/settings/hooks.html:9
1562 #: rhodecode/templates/admin/settings/settings.html:9
1563 #: rhodecode/templates/settings/repo_settings.html:5
1564 #: rhodecode/templates/settings/repo_settings.html:13
1565 msgid "Settings"
1566 msgstr "設定"
1567
1568 #: rhodecode/templates/admin/settings/hooks.html:24
1569 msgid "Built in hooks - read only"
1570 msgstr "內建hook - 唯讀"
1571
1572 #: rhodecode/templates/admin/settings/hooks.html:40
1573 msgid "Custom hooks"
1574 msgstr "自訂hook"
1575
1576 #: rhodecode/templates/admin/settings/hooks.html:56
1577 msgid "remove"
1578 msgstr "移除"
1579
1580 #: rhodecode/templates/admin/settings/hooks.html:88
1581 msgid "Failed to remove hook"
1582 msgstr "移除hook失敗"
1583
1584 #: rhodecode/templates/admin/settings/settings.html:24
1585 msgid "Remap and rescan repositories"
1586 msgstr "重新對映與掃描版本庫"
1587
1588 #: rhodecode/templates/admin/settings/settings.html:32
1589 msgid "rescan option"
1590 msgstr "重新掃描選項"
1591
1592 #: rhodecode/templates/admin/settings/settings.html:38
1593 msgid "In case a repository was deleted from filesystem and there are leftovers in the database check this option to scan obsolete data in database and remove it."
1594 msgstr "如果版本庫已從檔案系統中刪除,但是資料還留在資料庫,請勾選這個項目清理資料庫中舊的資料"
1595
1596 #: rhodecode/templates/admin/settings/settings.html:39
1597 msgid "destroy old data"
1598 msgstr "移除舊資料"
1599
1600 #: rhodecode/templates/admin/settings/settings.html:45
1601 msgid "Rescan repositories"
1602 msgstr "重新掃描版本庫"
1603
1604 #: rhodecode/templates/admin/settings/settings.html:51
1605 msgid "Whoosh indexing"
1606 msgstr "Whoosh 索引"
1607
1608 #: rhodecode/templates/admin/settings/settings.html:59
1609 msgid "index build option"
1610 msgstr "索引選項"
1611
1612 #: rhodecode/templates/admin/settings/settings.html:64
1613 msgid "build from scratch"
1614 msgstr "重頭建立索引"
1615
1616 #: rhodecode/templates/admin/settings/settings.html:70
1617 msgid "Reindex"
1618 msgstr "重新索引"
1619
1620 #: rhodecode/templates/admin/settings/settings.html:76
1621 msgid "Global application settings"
1622 msgstr "全域設定"
1623
1624 #: rhodecode/templates/admin/settings/settings.html:85
1625 msgid "Application name"
1626 msgstr "應用名稱"
1627
1628 #: rhodecode/templates/admin/settings/settings.html:94
1629 msgid "Realm text"
1630 msgstr ""
1631
1632 #: rhodecode/templates/admin/settings/settings.html:103
1633 msgid "GA code"
1634 msgstr ""
1635
1636 #: rhodecode/templates/admin/settings/settings.html:111
1637 #: rhodecode/templates/admin/settings/settings.html:177
1638 msgid "Save settings"
1639 msgstr "儲存設定"
1640
1641 #: rhodecode/templates/admin/settings/settings.html:112
1642 #: rhodecode/templates/admin/settings/settings.html:178
1643 #: rhodecode/templates/admin/users/user_edit.html:118
1644 #: rhodecode/templates/admin/users/user_edit.html:143
1645 #: rhodecode/templates/admin/users/user_edit_my_account.html:90
1646 #: rhodecode/templates/admin/users_groups/users_group_edit.html:264
1647 #: rhodecode/templates/files/files_edit.html:50
1648 msgid "Reset"
1649 msgstr "重設"
1650
1651 #: rhodecode/templates/admin/settings/settings.html:118
1652 msgid "Mercurial settings"
1653 msgstr "Mercurial 設定"
1654
1655 #: rhodecode/templates/admin/settings/settings.html:127
1656 msgid "Web"
1657 msgstr ""
1658
1659 #: rhodecode/templates/admin/settings/settings.html:132
1660 msgid "require ssl for pushing"
1661 msgstr "推送時要求使用SSL"
1662
1663 #: rhodecode/templates/admin/settings/settings.html:139
1664 msgid "Hooks"
1665 msgstr ""
1666
1667 #: rhodecode/templates/admin/settings/settings.html:142
1668 msgid "advanced setup"
1669 msgstr "進階設定"
1670
1671 #: rhodecode/templates/admin/settings/settings.html:147
1672 msgid "Update repository after push (hg update)"
1673 msgstr "push後更新版本庫 (hg update)"
1674
1675 #: rhodecode/templates/admin/settings/settings.html:151
1676 msgid "Show repository size after push"
1677 msgstr "push 後顯示版本庫大小"
1678
1679 #: rhodecode/templates/admin/settings/settings.html:155
1680 msgid "Log user push commands"
1681 msgstr "紀錄使用者推送命令"
1682
1683 #: rhodecode/templates/admin/settings/settings.html:159
1684 msgid "Log user pull commands"
1685 msgstr "紀錄使用者抓取命令"
1686
1687 #: rhodecode/templates/admin/settings/settings.html:166
1688 msgid "Repositories location"
1689 msgstr "版本庫路徑"
1690
1691 #: rhodecode/templates/admin/settings/settings.html:171
1692 msgid "This a crucial application setting. If you are really sure you need to change this, you must restart application in order to make this setting take effect. Click this label to unlock."
1693 msgstr "這是一個關鍵的設定,如果您確定要修改這個設定,請重新啟動應用程式以套用設定"
1694
1695 #: rhodecode/templates/admin/settings/settings.html:172
1696 msgid "unlock"
1697 msgstr "解鎖"
1698
1699 #: rhodecode/templates/admin/users/user_add.html:5
1700 msgid "Add user"
1701 msgstr "新增使用者"
1702
1703 #: rhodecode/templates/admin/users/user_add.html:10
1704 #: rhodecode/templates/admin/users/user_edit.html:11
1705 #: rhodecode/templates/admin/users/users.html:9
1706 msgid "Users"
1707 msgstr "使用者"
1708
1709 #: rhodecode/templates/admin/users/user_add.html:12
1710 msgid "add new user"
1711 msgstr "新增使用者"
1712
1713 #: rhodecode/templates/admin/users/user_add.html:77
1714 #: rhodecode/templates/admin/users/user_edit.html:101
1715 #: rhodecode/templates/admin/users_groups/users_group_add.html:41
1716 #: rhodecode/templates/admin/users_groups/users_group_edit.html:42
1717 msgid "Active"
1718 msgstr "啟用"
1719
1720 #: rhodecode/templates/admin/users/user_edit.html:5
1721 msgid "Edit user"
1722 msgstr "編輯使用者"
1723
1724 #: rhodecode/templates/admin/users/user_edit.html:33
1725 #: rhodecode/templates/admin/users/user_edit_my_account.html:32
1726 msgid "Change your avatar at"
1727 msgstr "修改您的頭像於"
1728
1729 #: rhodecode/templates/admin/users/user_edit.html:34
1730 #: rhodecode/templates/admin/users/user_edit_my_account.html:33
1731 msgid "Using"
1732 msgstr "使用中"
1733
1734 #: rhodecode/templates/admin/users/user_edit.html:40
1735 #: rhodecode/templates/admin/users/user_edit_my_account.html:39
1736 msgid "API key"
1737 msgstr ""
1738
1739 #: rhodecode/templates/admin/users/user_edit.html:56
1740 msgid "LDAP DN"
1741 msgstr ""
1742
1743 #: rhodecode/templates/admin/users/user_edit.html:65
1744 #: rhodecode/templates/admin/users/user_edit_my_account.html:54
1745 msgid "New password"
1746 msgstr "新密碼"
1747
1748 #: rhodecode/templates/admin/users/user_edit.html:135
1749 #: rhodecode/templates/admin/users_groups/users_group_edit.html:256
1750 msgid "Create repositories"
1751 msgstr "建立版本庫"
1752
1753 #: rhodecode/templates/admin/users/user_edit_my_account.html:5
1754 msgid "My account"
1755 msgstr "我的帳號"
1756
1757 #: rhodecode/templates/admin/users/user_edit_my_account.html:9
1758 msgid "My Account"
1759 msgstr "我的帳號"
1760
1761 #: rhodecode/templates/admin/users/user_edit_my_account.html:101
1762 msgid "My repositories"
1763 msgstr "我的版本庫"
1764
1765 #: rhodecode/templates/admin/users/user_edit_my_account.html:107
1766 msgid "ADD REPOSITORY"
1767 msgstr "新增版本庫"
1768
1769 #: rhodecode/templates/admin/users/user_edit_my_account.html:118
1770 #: rhodecode/templates/branches/branches_data.html:7
1771 #: rhodecode/templates/shortlog/shortlog_data.html:8
1772 #: rhodecode/templates/tags/tags_data.html:7
1773 msgid "revision"
1774 msgstr "修訂"
1775
1776 #: rhodecode/templates/admin/users/user_edit_my_account.html:157
1777 msgid "No repositories yet"
1778 msgstr "沒有任何版本庫"
1779
1780 #: rhodecode/templates/admin/users/user_edit_my_account.html:159
1781 msgid "create one now"
1782 msgstr ""
1783
1784 #: rhodecode/templates/admin/users/users.html:5
1785 msgid "Users administration"
1786 msgstr "使用者管理員"
1787
1788 #: rhodecode/templates/admin/users/users.html:23
1789 msgid "ADD NEW USER"
1790 msgstr "新增使用者"
1791
1792 #: rhodecode/templates/admin/users/users.html:33
1793 msgid "username"
1794 msgstr "使用者名稱"
1795
1796 #: rhodecode/templates/admin/users/users.html:34
1797 #: rhodecode/templates/branches/branches_data.html:5
1798 #: rhodecode/templates/tags/tags_data.html:5
1799 msgid "name"
1800 msgstr "名字"
1801
1802 #: rhodecode/templates/admin/users/users.html:35
1803 msgid "lastname"
1804 msgstr "姓"
1805
1806 #: rhodecode/templates/admin/users/users.html:36
1807 msgid "last login"
1808 msgstr "最後登入"
1809
1810 #: rhodecode/templates/admin/users/users.html:37
1811 #: rhodecode/templates/admin/users_groups/users_groups.html:34
1812 msgid "active"
1813 msgstr "啟用"
1814
1815 #: rhodecode/templates/admin/users/users.html:39
1816 #: rhodecode/templates/base/base.html:305
1817 msgid "ldap"
1818 msgstr ""
1819
1820 #: rhodecode/templates/admin/users/users.html:56
1821 msgid "Confirm to delete this user"
1822 msgstr "確認刪除這個使用者"
1823
1824 #: rhodecode/templates/admin/users_groups/users_group_add.html:5
1825 msgid "Add users group"
1826 msgstr "新增使用者群組"
1827
1828 #: rhodecode/templates/admin/users_groups/users_group_add.html:10
1829 #: rhodecode/templates/admin/users_groups/users_groups.html:9
1830 msgid "Users groups"
1831 msgstr "使用者群組"
1832
1833 #: rhodecode/templates/admin/users_groups/users_group_add.html:12
1834 msgid "add new users group"
1835 msgstr "新增使用者群組"
1836
1837 #: rhodecode/templates/admin/users_groups/users_group_edit.html:5
1838 msgid "Edit users group"
1839 msgstr "編輯使用者群組"
1840
1841 #: rhodecode/templates/admin/users_groups/users_group_edit.html:11
1842 msgid "UsersGroups"
1843 msgstr "使用者群組"
1844
1845 #: rhodecode/templates/admin/users_groups/users_group_edit.html:50
1846 msgid "Members"
1847 msgstr "成員"
1848
1849 #: rhodecode/templates/admin/users_groups/users_group_edit.html:58
1850 msgid "Choosen group members"
1851 msgstr "選擇群組成員"
1852
1853 #: rhodecode/templates/admin/users_groups/users_group_edit.html:61
1854 msgid "Remove all elements"
1855 msgstr "移除所有元素"
1856
1857 #: rhodecode/templates/admin/users_groups/users_group_edit.html:75
1858 msgid "Available members"
1859 msgstr "啟用的成員"
1860
1861 #: rhodecode/templates/admin/users_groups/users_group_edit.html:79
1862 msgid "Add all elements"
1863 msgstr "新增索有元素"
1864
1865 #: rhodecode/templates/admin/users_groups/users_groups.html:5
1866 msgid "Users groups administration"
1867 msgstr "使用者群組管理員"
1868
1869 #: rhodecode/templates/admin/users_groups/users_groups.html:23
1870 msgid "ADD NEW USER GROUP"
1871 msgstr "建立新的使用者群組"
1872
1873 #: rhodecode/templates/admin/users_groups/users_groups.html:32
1874 msgid "group name"
1875 msgstr "群組名稱"
1876
1877 #: rhodecode/templates/base/base.html:32
1878 msgid "Forgot password ?"
1879 msgstr "忘記密碼?"
1880
1881 #: rhodecode/templates/base/base.html:57
1882 #: rhodecode/templates/base/base.html:338
1883 #: rhodecode/templates/base/base.html:340
1884 #: rhodecode/templates/base/base.html:342
1885 msgid "Home"
1886 msgstr "首頁"
1887
1888 #: rhodecode/templates/base/base.html:61
1889 #: rhodecode/templates/base/base.html:347
1890 #: rhodecode/templates/base/base.html:349
1891 #: rhodecode/templates/base/base.html:351
1892 #: rhodecode/templates/journal/journal.html:4
1893 #: rhodecode/templates/journal/journal.html:17
1894 #: rhodecode/templates/journal/public_journal.html:4
1895 msgid "Journal"
1896 msgstr "日誌"
1897
1898 #: rhodecode/templates/base/base.html:66
1899 msgid "Login"
1900 msgstr "登入"
1901
1902 #: rhodecode/templates/base/base.html:68
1903 msgid "Log Out"
1904 msgstr "登出"
1905
1906 #: rhodecode/templates/base/base.html:107
1907 msgid "Submit a bug"
1908 msgstr "回報錯誤"
1909
1910 #: rhodecode/templates/base/base.html:141
1911 msgid "Switch repository"
1912 msgstr "切換版本庫"
1913
1914 #: rhodecode/templates/base/base.html:143
1915 msgid "Products"
1916 msgstr ""
1917
1918 #: rhodecode/templates/base/base.html:149
1919 msgid "loading..."
1920 msgstr "載入中..."
1921
1922 #: rhodecode/templates/base/base.html:234
1923 #: rhodecode/templates/base/base.html:236
1924 #: rhodecode/templates/base/base.html:238
1925 msgid "Switch to"
1926 msgstr "切換至"
1927
1928 #: rhodecode/templates/base/base.html:242
1929 #: rhodecode/templates/branches/branches.html:13
1930 msgid "branches"
1931 msgstr "分支"
1932
1933 #: rhodecode/templates/base/base.html:249
1934 #: rhodecode/templates/branches/branches_data.html:52
1935 msgid "There are no branches yet"
1936 msgstr "沒有任何分支"
1937
1938 #: rhodecode/templates/base/base.html:254
1939 #: rhodecode/templates/shortlog/shortlog_data.html:10
1940 #: rhodecode/templates/tags/tags.html:14
1941 msgid "tags"
1942 msgstr "標籤"
1943
1944 #: rhodecode/templates/base/base.html:261
1945 #: rhodecode/templates/tags/tags_data.html:32
1946 msgid "There are no tags yet"
1947 msgstr "沒有任何標籤"
1948
1949 #: rhodecode/templates/base/base.html:277
1950 #: rhodecode/templates/base/base.html:281
1951 #: rhodecode/templates/files/files_annotate.html:40
1952 #: rhodecode/templates/files/files_source.html:11
1953 msgid "Options"
1954 msgstr "選項"
1955
1956 #: rhodecode/templates/base/base.html:286
1957 #: rhodecode/templates/base/base.html:288
1958 #: rhodecode/templates/base/base.html:306
1959 msgid "settings"
1960 msgstr "設定"
1961
1962 #: rhodecode/templates/base/base.html:292
1963 msgid "search"
1964 msgstr "搜尋"
1965
1966 #: rhodecode/templates/base/base.html:299
1967 msgid "journal"
1968 msgstr "日誌"
1969
1970 #: rhodecode/templates/base/base.html:301
1971 msgid "repositories groups"
1972 msgstr "版本庫群組"
1973
1974 #: rhodecode/templates/base/base.html:302
1975 msgid "users"
1976 msgstr "使用者"
1977
1978 #: rhodecode/templates/base/base.html:303
1979 msgid "users groups"
1980 msgstr "使用者群組"
1981
1982 #: rhodecode/templates/base/base.html:304
1983 msgid "permissions"
1984 msgstr "權限"
1985
1986 #: rhodecode/templates/base/base.html:317
1987 #: rhodecode/templates/base/base.html:319
1988 #: rhodecode/templates/followers/followers.html:5
1989 msgid "Followers"
1990 msgstr "追蹤者"
1991
1992 #: rhodecode/templates/base/base.html:325
1993 #: rhodecode/templates/base/base.html:327
1994 #: rhodecode/templates/forks/forks.html:5
1995 msgid "Forks"
1996 msgstr ""
1997
1998 #: rhodecode/templates/base/base.html:356
1999 #: rhodecode/templates/base/base.html:358
2000 #: rhodecode/templates/base/base.html:360
2001 #: rhodecode/templates/search/search.html:4
2002 #: rhodecode/templates/search/search.html:24
2003 #: rhodecode/templates/search/search.html:46
2004 msgid "Search"
2005 msgstr "搜尋"
2006
2007 #: rhodecode/templates/base/root.html:57
2008 #: rhodecode/templates/journal/journal.html:48
2009 #: rhodecode/templates/summary/summary.html:36
2010 msgid "Stop following this repository"
2011 msgstr "停止追蹤這個版本庫"
2012
2013 #: rhodecode/templates/base/root.html:66
2014 #: rhodecode/templates/summary/summary.html:40
2015 msgid "Start following this repository"
2016 msgstr "開始追蹤這個版本庫"
2017
2018 #: rhodecode/templates/branches/branches_data.html:4
2019 #: rhodecode/templates/tags/tags_data.html:4
2020 msgid "date"
2021 msgstr "日期"
2022
2023 #: rhodecode/templates/branches/branches_data.html:6
2024 #: rhodecode/templates/shortlog/shortlog_data.html:7
2025 #: rhodecode/templates/tags/tags_data.html:6
2026 msgid "author"
2027 msgstr "作者"
2028
2029 #: rhodecode/templates/branches/branches_data.html:8
2030 #: rhodecode/templates/shortlog/shortlog_data.html:11
2031 #: rhodecode/templates/tags/tags_data.html:8
2032 msgid "links"
2033 msgstr "連結"
2034
2035 #: rhodecode/templates/branches/branches_data.html:23
2036 #: rhodecode/templates/branches/branches_data.html:43
2037 #: rhodecode/templates/shortlog/shortlog_data.html:39
2038 #: rhodecode/templates/tags/tags_data.html:24
2039 msgid "changeset"
2040 msgstr "修改"
2041
2042 #: rhodecode/templates/branches/branches_data.html:25
2043 #: rhodecode/templates/branches/branches_data.html:45
2044 #: rhodecode/templates/files/files.html:12
2045 #: rhodecode/templates/shortlog/shortlog_data.html:41
2046 #: rhodecode/templates/summary/summary.html:233
2047 #: rhodecode/templates/tags/tags_data.html:26
2048 msgid "files"
2049 msgstr "檔案"
2050
2051 #: rhodecode/templates/changelog/changelog.html:14
2052 msgid "showing "
2053 msgstr ""
2054
2055 #: rhodecode/templates/changelog/changelog.html:14
2056 msgid "out of"
2057 msgstr ""
2058
2059 #: rhodecode/templates/changelog/changelog.html:37
2060 msgid "Show"
2061 msgstr "顯示"
2062
2063 #: rhodecode/templates/changelog/changelog.html:50
2064 #: rhodecode/templates/changeset/changeset.html:42
2065 #: rhodecode/templates/summary/summary.html:609
2066 msgid "commit"
2067 msgstr "遞交"
2068
2069 #: rhodecode/templates/changelog/changelog.html:63
2070 msgid "Affected number of files, click to show more details"
2071 msgstr ""
2072
2073 #: rhodecode/templates/changelog/changelog.html:67
2074 #: rhodecode/templates/changeset/changeset.html:66
2075 msgid "merge"
2076 msgstr "合併"
2077
2078 #: rhodecode/templates/changelog/changelog.html:72
2079 #: rhodecode/templates/changeset/changeset.html:72
2080 msgid "Parent"
2081 msgstr ""
2082
2083 #: rhodecode/templates/changelog/changelog.html:77
2084 #: rhodecode/templates/changeset/changeset.html:77
2085 msgid "No parents"
2086 msgstr ""
2087
2088 #: rhodecode/templates/changelog/changelog.html:82
2089 #: rhodecode/templates/changeset/changeset.html:80
2090 #: rhodecode/templates/files/files.html:29
2091 #: rhodecode/templates/files/files_annotate.html:25
2092 #: rhodecode/templates/files/files_edit.html:33
2093 #: rhodecode/templates/shortlog/shortlog_data.html:9
2094 msgid "branch"
2095 msgstr "分支"
2096
2097 #: rhodecode/templates/changelog/changelog.html:86
2098 #: rhodecode/templates/changeset/changeset.html:83
2099 msgid "tag"
2100 msgstr "標籤"
2101
2102 #: rhodecode/templates/changelog/changelog.html:122
2103 msgid "Show selected changes __S -> __E"
2104 msgstr ""
2105
2106 #: rhodecode/templates/changelog/changelog.html:172
2107 #: rhodecode/templates/shortlog/shortlog_data.html:61
2108 msgid "There are no changes yet"
2109 msgstr "尚未有任何變更"
2110
2111 #: rhodecode/templates/changelog/changelog_details.html:2
2112 #: rhodecode/templates/changeset/changeset.html:55
2113 msgid "removed"
2114 msgstr "移除"
2115
2116 #: rhodecode/templates/changelog/changelog_details.html:3
2117 #: rhodecode/templates/changeset/changeset.html:56
2118 msgid "changed"
2119 msgstr "修改"
2120
2121 #: rhodecode/templates/changelog/changelog_details.html:4
2122 #: rhodecode/templates/changeset/changeset.html:57
2123 msgid "added"
2124 msgstr "新增"
2125
2126 #: rhodecode/templates/changelog/changelog_details.html:6
2127 #: rhodecode/templates/changelog/changelog_details.html:7
2128 #: rhodecode/templates/changelog/changelog_details.html:8
2129 #: rhodecode/templates/changeset/changeset.html:59
2130 #: rhodecode/templates/changeset/changeset.html:60
2131 #: rhodecode/templates/changeset/changeset.html:61
2132 #, python-format
2133 msgid "affected %s files"
2134 msgstr ""
2135
2136 #: rhodecode/templates/changeset/changeset.html:6
2137 #: rhodecode/templates/changeset/changeset.html:14
2138 #: rhodecode/templates/changeset/changeset.html:31
2139 msgid "Changeset"
2140 msgstr ""
2141
2142 #: rhodecode/templates/changeset/changeset.html:32
2143 #: rhodecode/templates/changeset/changeset.html:121
2144 #: rhodecode/templates/changeset/changeset_range.html:78
2145 #: rhodecode/templates/files/file_diff.html:32
2146 #: rhodecode/templates/files/file_diff.html:42
2147 msgid "raw diff"
2148 msgstr "原始差異"
2149
2150 #: rhodecode/templates/changeset/changeset.html:34
2151 #: rhodecode/templates/changeset/changeset.html:123
2152 #: rhodecode/templates/changeset/changeset_range.html:80
2153 #: rhodecode/templates/files/file_diff.html:34
2154 msgid "download diff"
2155 msgstr "下載差異"
2156
2157 #: rhodecode/templates/changeset/changeset.html:90
2158 #, python-format
2159 msgid "%s files affected with %s additions and %s deletions."
2160 msgstr ""
2161
2162 #: rhodecode/templates/changeset/changeset.html:101
2163 msgid "Changeset was too big and was cut off..."
2164 msgstr ""
2165
2166 #: rhodecode/templates/changeset/changeset.html:119
2167 #: rhodecode/templates/changeset/changeset_range.html:76
2168 #: rhodecode/templates/files/file_diff.html:30
2169 msgid "diff"
2170 msgstr "差異"
2171
2172 #: rhodecode/templates/changeset/changeset.html:132
2173 #: rhodecode/templates/changeset/changeset_range.html:89
2174 msgid "No changes in this file"
2175 msgstr "這個檔案沒有任何變更"
2176
2177 #: rhodecode/templates/changeset/changeset_range.html:30
2178 msgid "Compare View"
2179 msgstr "比較顯示"
2180
2181 #: rhodecode/templates/changeset/changeset_range.html:52
2182 msgid "Files affected"
2183 msgstr ""
2184
2185 #: rhodecode/templates/errors/error_document.html:44
2186 #, python-format
2187 msgid "You will be redirected to %s in %s seconds"
2188 msgstr ""
2189
2190 #: rhodecode/templates/files/file_diff.html:4
2191 #: rhodecode/templates/files/file_diff.html:12
2192 msgid "File diff"
2193 msgstr "檔案差異"
2194
2195 #: rhodecode/templates/files/file_diff.html:42
2196 msgid "Diff is to big to display"
2197 msgstr ""
2198
2199 #: rhodecode/templates/files/files.html:37
2200 #: rhodecode/templates/files/files_annotate.html:31
2201 #: rhodecode/templates/files/files_edit.html:39
2202 msgid "Location"
2203 msgstr "位置"
2204
2205 #: rhodecode/templates/files/files.html:46
2206 msgid "Go back"
2207 msgstr ""
2208
2209 #: rhodecode/templates/files/files.html:47
2210 msgid "No files at given path"
2211 msgstr ""
2212
2213 #: rhodecode/templates/files/files_annotate.html:4
2214 msgid "File annotate"
2215 msgstr "檔案註釋"
2216
2217 #: rhodecode/templates/files/files_annotate.html:12
2218 msgid "annotate"
2219 msgstr "註釋"
2220
2221 #: rhodecode/templates/files/files_annotate.html:33
2222 #: rhodecode/templates/files/files_browser.html:160
2223 #: rhodecode/templates/files/files_source.html:2
2224 msgid "Revision"
2225 msgstr "修訂"
2226
2227 #: rhodecode/templates/files/files_annotate.html:36
2228 #: rhodecode/templates/files/files_browser.html:158
2229 #: rhodecode/templates/files/files_source.html:7
2230 msgid "Size"
2231 msgstr "大小"
2232
2233 #: rhodecode/templates/files/files_annotate.html:38
2234 #: rhodecode/templates/files/files_browser.html:159
2235 #: rhodecode/templates/files/files_source.html:9
2236 msgid "Mimetype"
2237 msgstr ""
2238
2239 #: rhodecode/templates/files/files_annotate.html:41
2240 msgid "show source"
2241 msgstr "顯示原始碼"
2242
2243 #: rhodecode/templates/files/files_annotate.html:43
2244 #: rhodecode/templates/files/files_annotate.html:78
2245 #: rhodecode/templates/files/files_source.html:14
2246 #: rhodecode/templates/files/files_source.html:51
2247 msgid "show as raw"
2248 msgstr "顯示原始文件"
2249
2250 #: rhodecode/templates/files/files_annotate.html:45
2251 #: rhodecode/templates/files/files_source.html:16
2252 msgid "download as raw"
2253 msgstr "下載原始文件"
2254
2255 #: rhodecode/templates/files/files_annotate.html:54
2256 #: rhodecode/templates/files/files_source.html:25
2257 msgid "History"
2258 msgstr "歷史"
2259
2260 #: rhodecode/templates/files/files_annotate.html:73
2261 #: rhodecode/templates/files/files_source.html:46
2262 #, python-format
2263 msgid "Binary file (%s)"
2264 msgstr "二進位檔 (%s)"
2265
2266 #: rhodecode/templates/files/files_annotate.html:78
2267 #: rhodecode/templates/files/files_source.html:51
2268 msgid "File is too big to display"
2269 msgstr "顯示的檔案太大"
2270
2271 #: rhodecode/templates/files/files_browser.html:13
2272 msgid "view"
2273 msgstr "顯示"
2274
2275 #: rhodecode/templates/files/files_browser.html:14
2276 msgid "previous revision"
2277 msgstr "前一個修訂"
2278
2279 #: rhodecode/templates/files/files_browser.html:16
2280 msgid "next revision"
2281 msgstr "下一個修訂"
2282
2283 #: rhodecode/templates/files/files_browser.html:23
2284 msgid "follow current branch"
2285 msgstr ""
2286
2287 #: rhodecode/templates/files/files_browser.html:27
2288 msgid "search file list"
2289 msgstr "搜尋檔案列表"
2290
2291 #: rhodecode/templates/files/files_browser.html:32
2292 msgid "Loading file list..."
2293 msgstr "載入檔案列表..."
2294
2295 #: rhodecode/templates/files/files_browser.html:111
2296 msgid "search truncated"
2297 msgstr ""
2298
2299 #: rhodecode/templates/files/files_browser.html:122
2300 msgid "no matching files"
2301 msgstr "無符合的檔案"
2302
2303 #: rhodecode/templates/files/files_browser.html:161
2304 msgid "Last modified"
2305 msgstr "最後修改"
2306
2307 #: rhodecode/templates/files/files_browser.html:162
2308 msgid "Last commiter"
2309 msgstr "最後的遞交者"
2310
2311 #: rhodecode/templates/files/files_edit.html:4
2312 msgid "Edit file"
2313 msgstr "編輯檔案"
2314
2315 #: rhodecode/templates/files/files_edit.html:19
2316 msgid "edit file"
2317 msgstr "編輯檔案"
2318
2319 #: rhodecode/templates/files/files_edit.html:45
2320 #: rhodecode/templates/shortlog/shortlog_data.html:5
2321 msgid "commit message"
2322 msgstr "遞交資訊"
2323
2324 #: rhodecode/templates/files/files_edit.html:51
2325 msgid "Commit changes"
2326 msgstr "遞交修改"
2327
2328 #: rhodecode/templates/files/files_source.html:12
2329 msgid "show annotation"
2330 msgstr "險是註釋"
2331
2332 #: rhodecode/templates/files/files_source.html:153
2333 msgid "Selection link"
2334 msgstr ""
2335
2336 #: rhodecode/templates/followers/followers.html:13
2337 msgid "followers"
2338 msgstr "追蹤者"
2339
2340 #: rhodecode/templates/followers/followers_data.html:12
2341 msgid "Started following"
2342 msgstr "開始追蹤"
2343
2344 #: rhodecode/templates/forks/forks.html:13
2345 msgid "forks"
2346 msgstr "分支"
2347
2348 #: rhodecode/templates/forks/forks_data.html:17
2349 msgid "forked"
2350 msgstr "已建立分支"
2351
2352 #: rhodecode/templates/forks/forks_data.html:34
2353 msgid "There are no forks yet"
2354 msgstr "尚未有任何 fork"
2355
2356 #: rhodecode/templates/journal/journal.html:34
2357 msgid "Following"
2358 msgstr "已追蹤"
2359
2360 #: rhodecode/templates/journal/journal.html:41
2361 msgid "following user"
2362 msgstr "追蹤使用者"
2363
2364 #: rhodecode/templates/journal/journal.html:41
2365 msgid "user"
2366 msgstr "使用者"
2367
2368 #: rhodecode/templates/journal/journal.html:65
2369 msgid "You are not following any users or repositories"
2370 msgstr "您尚未追蹤任何使用者或版本庫"
2371
2372 #: rhodecode/templates/journal/journal_data.html:46
2373 msgid "No entries yet"
2374 msgstr ""
2375
2376 #: rhodecode/templates/journal/public_journal.html:17
2377 msgid "Public Journal"
2378 msgstr "開放日誌"
2379
2380 #: rhodecode/templates/search/search.html:7
2381 #: rhodecode/templates/search/search.html:26
2382 msgid "in repository: "
2383 msgstr "於版本庫:"
2384
2385 #: rhodecode/templates/search/search.html:9
2386 #: rhodecode/templates/search/search.html:28
2387 msgid "in all repositories"
2388 msgstr "於所有的版本庫"
2389
2390 #: rhodecode/templates/search/search.html:42
2391 msgid "Search term"
2392 msgstr "搜尋關鍵字"
2393
2394 #: rhodecode/templates/search/search.html:54
2395 msgid "Search in"
2396 msgstr "搜尋範圍"
2397
2398 #: rhodecode/templates/search/search.html:57
2399 msgid "File contents"
2400 msgstr "文件內容"
2401
2402 #: rhodecode/templates/search/search.html:59
2403 msgid "File names"
2404 msgstr "檔案名稱"
2405
2406 #: rhodecode/templates/search/search_content.html:20
2407 #: rhodecode/templates/search/search_path.html:15
2408 msgid "Permission denied"
2409 msgstr "權限不足"
2410
2411 #: rhodecode/templates/settings/repo_fork.html:5
2412 msgid "Fork"
2413 msgstr "分支"
2414
2415 #: rhodecode/templates/settings/repo_fork.html:31
2416 msgid "Fork name"
2417 msgstr "分支名稱"
2418
2419 #: rhodecode/templates/settings/repo_fork.html:55
2420 msgid "fork this repository"
2421 msgstr "fork 這個版本庫"
2422
2423 #: rhodecode/templates/shortlog/shortlog.html:5
2424 #: rhodecode/templates/summary/summary.html:666
2425 msgid "Shortlog"
2426 msgstr "簡短紀錄"
2427
2428 #: rhodecode/templates/shortlog/shortlog.html:14
2429 msgid "shortlog"
2430 msgstr "簡短紀錄"
2431
2432 #: rhodecode/templates/shortlog/shortlog_data.html:6
2433 msgid "age"
2434 msgstr ""
2435
2436 #: rhodecode/templates/summary/summary.html:12
2437 msgid "summary"
2438 msgstr "概況"
2439
2440 #: rhodecode/templates/summary/summary.html:79
2441 msgid "remote clone"
2442 msgstr "遠端複製"
2443
2444 #: rhodecode/templates/summary/summary.html:121
2445 msgid "by"
2446 msgstr ""
2447
2448 #: rhodecode/templates/summary/summary.html:128
2449 msgid "Clone url"
2450 msgstr "複製連結"
2451
2452 #: rhodecode/templates/summary/summary.html:137
2453 msgid "Trending source files"
2454 msgstr ""
2455
2456 #: rhodecode/templates/summary/summary.html:146
2457 msgid "Download"
2458 msgstr "下載"
2459
2460 #: rhodecode/templates/summary/summary.html:150
2461 msgid "There are no downloads yet"
2462 msgstr "沒有任何下載"
2463
2464 #: rhodecode/templates/summary/summary.html:152
2465 msgid "Downloads are disabled for this repository"
2466 msgstr "這個版本庫的下載已停用"
2467
2468 #: rhodecode/templates/summary/summary.html:154
2469 #: rhodecode/templates/summary/summary.html:320
2470 msgid "enable"
2471 msgstr "啟用"
2472
2473 #: rhodecode/templates/summary/summary.html:162
2474 #: rhodecode/templates/summary/summary.html:297
2475 #, python-format
2476 msgid "Download %s as %s"
2477 msgstr "下載 %s 為 %s"
2478
2479 #: rhodecode/templates/summary/summary.html:168
2480 msgid "Check this to download archive with subrepos"
2481 msgstr ""
2482
2483 #: rhodecode/templates/summary/summary.html:168
2484 msgid "with subrepos"
2485 msgstr ""
2486
2487 #: rhodecode/templates/summary/summary.html:176
2488 msgid "Feeds"
2489 msgstr ""
2490
2491 #: rhodecode/templates/summary/summary.html:257
2492 #: rhodecode/templates/summary/summary.html:684
2493 #: rhodecode/templates/summary/summary.html:695
2494 msgid "show more"
2495 msgstr "顯示更多"
2496
2497 #: rhodecode/templates/summary/summary.html:312
2498 msgid "Commit activity by day / author"
2499 msgstr ""
2500
2501 #: rhodecode/templates/summary/summary.html:324
2502 msgid "Loaded in"
2503 msgstr ""
2504
2505 #: rhodecode/templates/summary/summary.html:603
2506 msgid "commits"
2507 msgstr "遞交"
2508
2509 #: rhodecode/templates/summary/summary.html:604
2510 msgid "files added"
2511 msgstr "多個檔案新增"
2512
2513 #: rhodecode/templates/summary/summary.html:605
2514 msgid "files changed"
2515 msgstr "多個檔案修改"
2516
2517 #: rhodecode/templates/summary/summary.html:606
2518 msgid "files removed"
2519 msgstr "移除多個檔案"
2520
2521 #: rhodecode/templates/summary/summary.html:610
2522 msgid "file added"
2523 msgstr "檔案新增"
2524
2525 #: rhodecode/templates/summary/summary.html:611
2526 msgid "file changed"
2527 msgstr "檔案修改"
2528
2529 #: rhodecode/templates/summary/summary.html:612
2530 msgid "file removed"
2531 msgstr "移除檔案"
2532
@@ -1,20 +1,21 b''
1 1 List of contributors to RhodeCode project:
2 2 Marcin Kuźmiński <marcin@python-works.com>
3 3 Lukasz Balcerzak <lukaszbalcerzak@gmail.com>
4 4 Jason Harris <jason@jasonfharris.com>
5 5 Thayne Harbaugh <thayne@fusionio.com>
6 cejones
6 cejones <>
7 7 Thomas Waldmann <tw-public@gmx.de>
8 8 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
9 Dmitri Kuznetsov
9 Dmitri Kuznetsov <>
10 10 Jared Bunting <jared.bunting@peachjean.com>
11 11 Steve Romanow <slestak989@gmail.com>
12 12 Augosto Hermann <augusto.herrmann@planejamento.gov.br>
13 13 Ankit Solanki <ankit.solanki@gmail.com>
14 14 Liad Shani <liadff@gmail.com>
15 15 Les Peabody <lpeabody@gmail.com>
16 16 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
17 17 Matt Zuba <matt.zuba@goodwillaz.org>
18 18 Aras Pranckevicius <aras@unity3d.com>
19 19 Tony Bussieres <t.bussieres@gmail.com>
20 Erwin Kroon <e.kroon@smartmetersolutions.nl> No newline at end of file
20 Erwin Kroon <e.kroon@smartmetersolutions.nl>
21 nansenat16 <nansenat16@null.tw> No newline at end of file
@@ -1,174 +1,174 b''
1 1 =========
2 2 RhodeCode
3 3 =========
4 4
5 5 About
6 6 -----
7 7
8 8 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
9 9 with a built in push/pull server and full text search and code-review.
10 10 It works on http/https and has a built in permission/authentication system with
11 11 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides
12 12 simple API so it's easy integrable with existing external systems.
13 13
14 14 RhodeCode is similar in some respects to github_ or bitbucket_,
15 15 however RhodeCode can be run as standalone hosted application on your own server.
16 16 It is open source and donation ware and focuses more on providing a customized,
17 17 self administered interface for Mercurial_ and GIT_ repositories.
18 RhodeCode works on *nix systems and Windows it is powered by a vcs_ library
18 RhodeCode works on \*nix systems and Windows it is powered by a vcs_ library
19 19 that Lukasz Balcerzak and Marcin Kuzminski created to handle multiple
20 20 different version control systems.
21 21
22 22 RhodeCode uses `PEP386 versioning <http://www.python.org/dev/peps/pep-0386/>`_
23 23
24 24 Installation
25 25 ------------
26 26 Stable releases of RhodeCode are best installed via::
27 27
28 28 easy_install rhodecode
29 29
30 30 Or::
31 31
32 32 pip install rhodecode
33 33
34 34 Detailed instructions and links may be found on the Installation page.
35 35
36 36 Please visit http://packages.python.org/RhodeCode/installation.html for
37 37 more details
38 38
39 39 RhodeCode demo
40 40 --------------
41 41
42 42 http://demo.rhodecode.org
43 43
44 44 The default access is anonymous but you can login to an administrative account
45 45 using the following credentials:
46 46
47 47 - username: demo
48 48 - password: demo12
49 49
50 50 Source code
51 51 -----------
52 52
53 53 The latest sources can be obtained from official RhodeCode instance
54 54 https://secure.rhodecode.org
55 55
56 56
57 57 MIRRORS:
58 58
59 59 Issue tracker and sources at bitbucket_
60 60
61 61 http://bitbucket.org/marcinkuzminski/rhodecode
62 62
63 63 Sources at github_
64 64
65 65 https://github.com/marcinkuzminski/rhodecode
66 66
67 67
68 68 RhodeCode Features
69 69 ------------------
70 70
71 71 - Has its own middleware to handle mercurial_ protocol requests.
72 72 Each request can be logged and authenticated.
73 73 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
74 74 Supports http/https and LDAP
75 75 - Full permissions (private/read/write/admin) and authentication per project.
76 76 One account for web interface and mercurial_ push/pull/clone operations.
77 77 - Have built in users groups for easier permission management
78 78 - Repository groups let you group repos and manage them easier.
79 79 - Users can fork other users repo. RhodeCode have also compare view to see
80 80 combined changeset for all changeset made within single push.
81 81 - Build in commit-api let's you add, edit and commit files right from RhodeCode
82 82 interface using simple editor or upload form for binaries.
83 83 - Mako templates let's you customize the look and feel of the application.
84 84 - Beautiful diffs, annotations and source code browsing all colored by pygments.
85 85 Raw diffs are made in git-diff format, including git_ binary-patches
86 86 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
87 87 - Admin interface with user/permission management. Admin activity journal, logs
88 88 pulls, pushes, forks, registrations and other actions made by all users.
89 89 - Server side forks. It is possible to fork a project and modify it freely
90 90 without breaking the main repository. You can even write Your own hooks
91 91 and install them
92 92 - code review with notification system, inline commenting, all parsed using
93 93 rst syntax
94 94 - rst and markdown README support for repositories
95 95 - Full text search powered by Whoosh on the source files, and file names.
96 96 Build in indexing daemons, with optional incremental index build
97 97 (no external search servers required all in one application)
98 98 - Setup project descriptions and info inside built in db for easy, non
99 99 file-system operations
100 100 - Intelligent cache with invalidation after push or project change, provides
101 101 high performance and always up to date data.
102 102 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
103 103 - Optional async tasks for speed and performance using celery_
104 104 - Backup scripts can do backup of whole app and send it over scp to desired
105 105 location
106 106 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
107 107
108 108
109 109 Incoming / Plans
110 110 ----------------
111 111
112 112 - Finer granular permissions per branch, repo group or subrepo
113 113 - pull requests and web based merges
114 114 - per line file history
115 115 - SSH based authentication with server side key management
116 116 - Commit based built in wiki system
117 117 - More statistics and graph (global annotation + some more statistics)
118 118 - Other advancements as development continues (or you can of course make
119 119 additions and or requests)
120 120
121 121 License
122 122 -------
123 123
124 124 ``RhodeCode`` is released under the GPLv3 license.
125 125
126 126
127 127 Getting help
128 128 ------------
129 129
130 130 Listed bellow are various support resources that should help.
131 131
132 132 .. note::
133 133
134 134 Please try to read the documentation before posting any issues
135 135
136 136 - Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
137 137 any questions.
138 138
139 139 - Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
140 140
141 141
142 142 - Join #rhodecode on FreeNode (irc.freenode.net)
143 143 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
144 144
145 145 - You can also follow me on twitter **@marcinkuzminski** where i often post some
146 146 news about RhodeCode
147 147
148 148
149 149 Online documentation
150 150 --------------------
151 151
152 152 Online documentation for the current version of RhodeCode is available at
153 153 - http://packages.python.org/RhodeCode/
154 154 - http://rhodecode.readthedocs.org/en/latest/index.html
155 155
156 156 You may also build the documentation for yourself - go into ``docs/`` and run::
157 157
158 158 make html
159 159
160 160 (You need to have sphinx_ installed to build the documentation. If you don't
161 161 have sphinx_ installed you can install it via the command:
162 162 ``easy_install sphinx``)
163 163
164 164 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
165 165 .. _python: http://www.python.org/
166 166 .. _sphinx: http://sphinx.pocoo.org/
167 167 .. _mercurial: http://mercurial.selenic.com/
168 168 .. _bitbucket: http://bitbucket.org/
169 169 .. _github: http://github.com/
170 170 .. _subversion: http://subversion.tigris.org/
171 171 .. _git: http://git-scm.com/
172 172 .. _celery: http://celeryproject.org/
173 173 .. _Sphinx: http://sphinx.pocoo.org/
174 174 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,633 +1,661 b''
1 1 .. _changelog:
2 2
3 3 =========
4 4 Changelog
5 5 =========
6 6
7 1.3.5 (**2012-XX-XX**)
7 1.4.0 (**2012-XX-XX**)
8 8 ----------------------
9 9
10 10 :status: in-progress
11 11 :branch: beta
12 12
13 13 news
14 14 ++++
15 15
16 fixes
17 +++++
18
19 1.3.6 (**2012-05-16**)
20 ----------------------
21
22 news
23 ++++
24
25 - chinese traditional translation
26
27 fixes
28 +++++
29
30 - fixed no scm found warning
31 - fixed __future__ import error on rcextensions
32 - made simplejson required lib for speedup on JSON encoding
33 - fixes #449 bad regex could get more than revisions from parsing history
34
35 1.3.5 (**2012-05-10**)
36 ----------------------
37
38 news
39 ++++
40
16 41 - use ext_json for json module
17 42 - unified annotation view with file source view
18 43 - notification improvements, better inbox + css
19 44 - #419 don't strip passwords for login forms, make rhodecode
20 45 more compatible with LDAP servers
21 46 - Added HTTP_X_FORWARDED_FOR as another method of extracting
22 47 IP for pull/push logs. - moved all to base controller
23 48 - #415: Adding comment to changeset causes reload.
24 49 Comments are now added via ajax and doesn't reload the page
25 50 - #374 LDAP config is discarded when LDAP can't be activated
26 51 - limited push/pull operations are now logged for git in the journal
27 52 - bumped mercurial to 2.2.X series
28 53 - added support for displaying submodules in file-browser
54 - #421 added bookmarks in changelog view
29 55
30 56 fixes
31 57 +++++
32 58
33 59 - fixed dev-version marker for stable when served from source codes
34 60 - fixed missing permission checks on show forks page
35 61 - #418 cast to unicode fixes in notification objects
36 62 - #426 fixed mention extracting regex
37 63 - fixed remote-pulling for git remotes remopositories
38 64 - fixed #434: Error when accessing files or changesets of a git repository
39 65 with submodules
66 - fixed issue with empty APIKEYS for users after registration ref. #438
67 - fixed issue with getting README files from git repositories
40 68
41 69 1.3.4 (**2012-03-28**)
42 70 ----------------------
43 71
44 72 news
45 73 ++++
46 74
47 75 - Whoosh logging is now controlled by the .ini files logging setup
48 76 - added clone-url into edit form on /settings page
49 77 - added help text into repo add/edit forms
50 78 - created rcextensions module with additional mappings (ref #322) and
51 79 post push/pull/create repo hooks callbacks
52 80 - implemented #377 Users view for his own permissions on account page
53 81 - #399 added inheritance of permissions for users group on repos groups
54 82 - #401 repository group is automatically pre-selected when adding repos
55 83 inside a repository group
56 84 - added alternative HTTP 403 response when client failed to authenticate. Helps
57 85 solving issues with Mercurial and LDAP
58 86 - #402 removed group prefix from repository name when listing repositories
59 87 inside a group
60 88 - added gravatars into permission view and permissions autocomplete
61 89 - #347 when running multiple RhodeCode instances, properly invalidates cache
62 90 for all registered servers
63 91
64 92 fixes
65 93 +++++
66 94
67 95 - fixed #390 cache invalidation problems on repos inside group
68 96 - fixed #385 clone by ID url was loosing proxy prefix in URL
69 97 - fixed some unicode problems with waitress
70 98 - fixed issue with escaping < and > in changeset commits
71 99 - fixed error occurring during recursive group creation in API
72 100 create_repo function
73 101 - fixed #393 py2.5 fixes for routes url generator
74 102 - fixed #397 Private repository groups shows up before login
75 103 - fixed #396 fixed problems with revoking users in nested groups
76 104 - fixed mysql unicode issues + specified InnoDB as default engine with
77 105 utf8 charset
78 106 - #406 trim long branch/tag names in changelog to not break UI
79 107
80 108 1.3.3 (**2012-03-02**)
81 109 ----------------------
82 110
83 111 news
84 112 ++++
85 113
86 114
87 115 fixes
88 116 +++++
89 117
90 118 - fixed some python2.5 compatibility issues
91 119 - fixed issues with removed repos was accidentally added as groups, after
92 120 full rescan of paths
93 121 - fixes #376 Cannot edit user (using container auth)
94 122 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
95 123 configuration
96 124 - fixed initial sorting of repos inside repo group
97 125 - fixes issue when user tried to resubmit same permission into user/user_groups
98 126 - bumped beaker version that fixes #375 leap error bug
99 127 - fixed raw_changeset for git. It was generated with hg patch headers
100 128 - fixed vcs issue with last_changeset for filenodes
101 129 - fixed missing commit after hook delete
102 130 - fixed #372 issues with git operation detection that caused a security issue
103 131 for git repos
104 132
105 133 1.3.2 (**2012-02-28**)
106 134 ----------------------
107 135
108 136 news
109 137 ++++
110 138
111 139
112 140 fixes
113 141 +++++
114 142
115 143 - fixed git protocol issues with repos-groups
116 144 - fixed git remote repos validator that prevented from cloning remote git repos
117 145 - fixes #370 ending slashes fixes for repo and groups
118 146 - fixes #368 improved git-protocol detection to handle other clients
119 147 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
120 148 Moved To Root
121 149 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
122 150 - fixed #373 missing cascade drop on user_group_to_perm table
123 151
124 152 1.3.1 (**2012-02-27**)
125 153 ----------------------
126 154
127 155 news
128 156 ++++
129 157
130 158
131 159 fixes
132 160 +++++
133 161
134 162 - redirection loop occurs when remember-me wasn't checked during login
135 163 - fixes issues with git blob history generation
136 164 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
137 165
138 166 1.3.0 (**2012-02-26**)
139 167 ----------------------
140 168
141 169 news
142 170 ++++
143 171
144 172 - code review, inspired by github code-comments
145 173 - #215 rst and markdown README files support
146 174 - #252 Container-based and proxy pass-through authentication support
147 175 - #44 branch browser. Filtering of changelog by branches
148 176 - mercurial bookmarks support
149 177 - new hover top menu, optimized to add maximum size for important views
150 178 - configurable clone url template with possibility to specify protocol like
151 179 ssh:// or http:// and also manually alter other parts of clone_url.
152 180 - enabled largefiles extension by default
153 181 - optimized summary file pages and saved a lot of unused space in them
154 182 - #239 option to manually mark repository as fork
155 183 - #320 mapping of commit authors to RhodeCode users
156 184 - #304 hashes are displayed using monospace font
157 185 - diff configuration, toggle white lines and context lines
158 186 - #307 configurable diffs, whitespace toggle, increasing context lines
159 187 - sorting on branches, tags and bookmarks using YUI datatable
160 188 - improved file filter on files page
161 189 - implements #330 api method for listing nodes ar particular revision
162 190 - #73 added linking issues in commit messages to chosen issue tracker url
163 191 based on user defined regular expression
164 192 - added linking of changesets in commit messages
165 193 - new compact changelog with expandable commit messages
166 194 - firstname and lastname are optional in user creation
167 195 - #348 added post-create repository hook
168 196 - #212 global encoding settings is now configurable from .ini files
169 197 - #227 added repository groups permissions
170 198 - markdown gets codehilite extensions
171 199 - new API methods, delete_repositories, grante/revoke permissions for groups
172 200 and repos
173 201
174 202
175 203 fixes
176 204 +++++
177 205
178 206 - rewrote dbsession management for atomic operations, and better error handling
179 207 - fixed sorting of repo tables
180 208 - #326 escape of special html entities in diffs
181 209 - normalized user_name => username in api attributes
182 210 - fixes #298 ldap created users with mixed case emails created conflicts
183 211 on saving a form
184 212 - fixes issue when owner of a repo couldn't revoke permissions for users
185 213 and groups
186 214 - fixes #271 rare JSON serialization problem with statistics
187 215 - fixes #337 missing validation check for conflicting names of a group with a
188 216 repositories group
189 217 - #340 fixed session problem for mysql and celery tasks
190 218 - fixed #331 RhodeCode mangles repository names if the a repository group
191 219 contains the "full path" to the repositories
192 220 - #355 RhodeCode doesn't store encrypted LDAP passwords
193 221
194 222 1.2.5 (**2012-01-28**)
195 223 ----------------------
196 224
197 225 news
198 226 ++++
199 227
200 228 fixes
201 229 +++++
202 230
203 231 - #340 Celery complains about MySQL server gone away, added session cleanup
204 232 for celery tasks
205 233 - #341 "scanning for repositories in None" log message during Rescan was missing
206 234 a parameter
207 235 - fixed creating archives with subrepos. Some hooks were triggered during that
208 236 operation leading to crash.
209 237 - fixed missing email in account page.
210 238 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
211 239 forking on windows impossible
212 240
213 241 1.2.4 (**2012-01-19**)
214 242 ----------------------
215 243
216 244 news
217 245 ++++
218 246
219 247 - RhodeCode is bundled with mercurial series 2.0.X by default, with
220 248 full support to largefiles extension. Enabled by default in new installations
221 249 - #329 Ability to Add/Remove Groups to/from a Repository via AP
222 250 - added requires.txt file with requirements
223 251
224 252 fixes
225 253 +++++
226 254
227 255 - fixes db session issues with celery when emailing admins
228 256 - #331 RhodeCode mangles repository names if the a repository group
229 257 contains the "full path" to the repositories
230 258 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
231 259 - DB session cleanup after hg protocol operations, fixes issues with
232 260 `mysql has gone away` errors
233 261 - #333 doc fixes for get_repo api function
234 262 - #271 rare JSON serialization problem with statistics enabled
235 263 - #337 Fixes issues with validation of repository name conflicting with
236 264 a group name. A proper message is now displayed.
237 265 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
238 266 doesn't work
239 267 - #316 fixes issues with web description in hgrc files
240 268
241 269 1.2.3 (**2011-11-02**)
242 270 ----------------------
243 271
244 272 news
245 273 ++++
246 274
247 275 - added option to manage repos group for non admin users
248 276 - added following API methods for get_users, create_user, get_users_groups,
249 277 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
250 278 get_repo, create_repo, add_user_to_repo
251 279 - implements #237 added password confirmation for my account
252 280 and admin edit user.
253 281 - implements #291 email notification for global events are now sent to all
254 282 administrator users, and global config email.
255 283
256 284 fixes
257 285 +++++
258 286
259 287 - added option for passing auth method for smtp mailer
260 288 - #276 issue with adding a single user with id>10 to usergroups
261 289 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
262 290 - #288 fixes managing of repos in a group for non admin user
263 291
264 292 1.2.2 (**2011-10-17**)
265 293 ----------------------
266 294
267 295 news
268 296 ++++
269 297
270 298 - #226 repo groups are available by path instead of numerical id
271 299
272 300 fixes
273 301 +++++
274 302
275 303 - #259 Groups with the same name but with different parent group
276 304 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
277 305 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
278 306 - #265 ldap save fails sometimes on converting attributes to booleans,
279 307 added getter and setter into model that will prevent from this on db model level
280 308 - fixed problems with timestamps issues #251 and #213
281 309 - fixes #266 RhodeCode allows to create repo with the same name and in
282 310 the same parent as group
283 311 - fixes #245 Rescan of the repositories on Windows
284 312 - fixes #248 cannot edit repos inside a group on windows
285 313 - fixes #219 forking problems on windows
286 314
287 315 1.2.1 (**2011-10-08**)
288 316 ----------------------
289 317
290 318 news
291 319 ++++
292 320
293 321
294 322 fixes
295 323 +++++
296 324
297 325 - fixed problems with basic auth and push problems
298 326 - gui fixes
299 327 - fixed logger
300 328
301 329 1.2.0 (**2011-10-07**)
302 330 ----------------------
303 331
304 332 news
305 333 ++++
306 334
307 335 - implemented #47 repository groups
308 336 - implemented #89 Can setup google analytics code from settings menu
309 337 - implemented #91 added nicer looking archive urls with more download options
310 338 like tags, branches
311 339 - implemented #44 into file browsing, and added follow branch option
312 340 - implemented #84 downloads can be enabled/disabled for each repository
313 341 - anonymous repository can be cloned without having to pass default:default
314 342 into clone url
315 343 - fixed #90 whoosh indexer can index chooses repositories passed in command
316 344 line
317 345 - extended journal with day aggregates and paging
318 346 - implemented #107 source code lines highlight ranges
319 347 - implemented #93 customizable changelog on combined revision ranges -
320 348 equivalent of githubs compare view
321 349 - implemented #108 extended and more powerful LDAP configuration
322 350 - implemented #56 users groups
323 351 - major code rewrites optimized codes for speed and memory usage
324 352 - raw and diff downloads are now in git format
325 353 - setup command checks for write access to given path
326 354 - fixed many issues with international characters and unicode. It uses utf8
327 355 decode with replace to provide less errors even with non utf8 encoded strings
328 356 - #125 added API KEY access to feeds
329 357 - #109 Repository can be created from external Mercurial link (aka. remote
330 358 repository, and manually updated (via pull) from admin panel
331 359 - beta git support - push/pull server + basic view for git repos
332 360 - added followers page and forks page
333 361 - server side file creation (with binary file upload interface)
334 362 and edition with commits powered by codemirror
335 363 - #111 file browser file finder, quick lookup files on whole file tree
336 364 - added quick login sliding menu into main page
337 365 - changelog uses lazy loading of affected files details, in some scenarios
338 366 this can improve speed of changelog page dramatically especially for
339 367 larger repositories.
340 368 - implements #214 added support for downloading subrepos in download menu.
341 369 - Added basic API for direct operations on rhodecode via JSON
342 370 - Implemented advanced hook management
343 371
344 372 fixes
345 373 +++++
346 374
347 375 - fixed file browser bug, when switching into given form revision the url was
348 376 not changing
349 377 - fixed propagation to error controller on simplehg and simplegit middlewares
350 378 - fixed error when trying to make a download on empty repository
351 379 - fixed problem with '[' chars in commit messages in journal
352 380 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
353 381 - journal fork fixes
354 382 - removed issue with space inside renamed repository after deletion
355 383 - fixed strange issue on formencode imports
356 384 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
357 385 - #150 fixes for errors on repositories mapped in db but corrupted in
358 386 filesystem
359 387 - fixed problem with ascendant characters in realm #181
360 388 - fixed problem with sqlite file based database connection pool
361 389 - whoosh indexer and code stats share the same dynamic extensions map
362 390 - fixes #188 - relationship delete of repo_to_perm entry on user removal
363 391 - fixes issue #189 Trending source files shows "show more" when no more exist
364 392 - fixes issue #197 Relative paths for pidlocks
365 393 - fixes issue #198 password will require only 3 chars now for login form
366 394 - fixes issue #199 wrong redirection for non admin users after creating a repository
367 395 - fixes issues #202, bad db constraint made impossible to attach same group
368 396 more than one time. Affects only mysql/postgres
369 397 - fixes #218 os.kill patch for windows was missing sig param
370 398 - improved rendering of dag (they are not trimmed anymore when number of
371 399 heads exceeds 5)
372 400
373 401 1.1.8 (**2011-04-12**)
374 402 ----------------------
375 403
376 404 news
377 405 ++++
378 406
379 407 - improved windows support
380 408
381 409 fixes
382 410 +++++
383 411
384 412 - fixed #140 freeze of python dateutil library, since new version is python2.x
385 413 incompatible
386 414 - setup-app will check for write permission in given path
387 415 - cleaned up license info issue #149
388 416 - fixes for issues #137,#116 and problems with unicode and accented characters.
389 417 - fixes crashes on gravatar, when passed in email as unicode
390 418 - fixed tooltip flickering problems
391 419 - fixed came_from redirection on windows
392 420 - fixed logging modules, and sql formatters
393 421 - windows fixes for os.kill issue #133
394 422 - fixes path splitting for windows issues #148
395 423 - fixed issue #143 wrong import on migration to 1.1.X
396 424 - fixed problems with displaying binary files, thanks to Thomas Waldmann
397 425 - removed name from archive files since it's breaking ui for long repo names
398 426 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
399 427 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
400 428 Thomas Waldmann
401 429 - fixed issue #166 summary pager was skipping 10 revisions on second page
402 430
403 431
404 432 1.1.7 (**2011-03-23**)
405 433 ----------------------
406 434
407 435 news
408 436 ++++
409 437
410 438 fixes
411 439 +++++
412 440
413 441 - fixed (again) #136 installation support for FreeBSD
414 442
415 443
416 444 1.1.6 (**2011-03-21**)
417 445 ----------------------
418 446
419 447 news
420 448 ++++
421 449
422 450 fixes
423 451 +++++
424 452
425 453 - fixed #136 installation support for FreeBSD
426 454 - RhodeCode will check for python version during installation
427 455
428 456 1.1.5 (**2011-03-17**)
429 457 ----------------------
430 458
431 459 news
432 460 ++++
433 461
434 462 - basic windows support, by exchanging pybcrypt into sha256 for windows only
435 463 highly inspired by idea of mantis406
436 464
437 465 fixes
438 466 +++++
439 467
440 468 - fixed sorting by author in main page
441 469 - fixed crashes with diffs on binary files
442 470 - fixed #131 problem with boolean values for LDAP
443 471 - fixed #122 mysql problems thanks to striker69
444 472 - fixed problem with errors on calling raw/raw_files/annotate functions
445 473 with unknown revisions
446 474 - fixed returned rawfiles attachment names with international character
447 475 - cleaned out docs, big thanks to Jason Harris
448 476
449 477 1.1.4 (**2011-02-19**)
450 478 ----------------------
451 479
452 480 news
453 481 ++++
454 482
455 483 fixes
456 484 +++++
457 485
458 486 - fixed formencode import problem on settings page, that caused server crash
459 487 when that page was accessed as first after server start
460 488 - journal fixes
461 489 - fixed option to access repository just by entering http://server/<repo_name>
462 490
463 491 1.1.3 (**2011-02-16**)
464 492 ----------------------
465 493
466 494 news
467 495 ++++
468 496
469 497 - implemented #102 allowing the '.' character in username
470 498 - added option to access repository just by entering http://server/<repo_name>
471 499 - celery task ignores result for better performance
472 500
473 501 fixes
474 502 +++++
475 503
476 504 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
477 505 apollo13 and Johan Walles
478 506 - small fixes in journal
479 507 - fixed problems with getting setting for celery from .ini files
480 508 - registration, password reset and login boxes share the same title as main
481 509 application now
482 510 - fixed #113: to high permissions to fork repository
483 511 - fixed problem with '[' chars in commit messages in journal
484 512 - removed issue with space inside renamed repository after deletion
485 513 - db transaction fixes when filesystem repository creation failed
486 514 - fixed #106 relation issues on databases different than sqlite
487 515 - fixed static files paths links to use of url() method
488 516
489 517 1.1.2 (**2011-01-12**)
490 518 ----------------------
491 519
492 520 news
493 521 ++++
494 522
495 523
496 524 fixes
497 525 +++++
498 526
499 527 - fixes #98 protection against float division of percentage stats
500 528 - fixed graph bug
501 529 - forced webhelpers version since it was making troubles during installation
502 530
503 531 1.1.1 (**2011-01-06**)
504 532 ----------------------
505 533
506 534 news
507 535 ++++
508 536
509 537 - added force https option into ini files for easier https usage (no need to
510 538 set server headers with this options)
511 539 - small css updates
512 540
513 541 fixes
514 542 +++++
515 543
516 544 - fixed #96 redirect loop on files view on repositories without changesets
517 545 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
518 546 and server crashed with errors
519 547 - fixed large tooltips problems on main page
520 548 - fixed #92 whoosh indexer is more error proof
521 549
522 550 1.1.0 (**2010-12-18**)
523 551 ----------------------
524 552
525 553 news
526 554 ++++
527 555
528 556 - rewrite of internals for vcs >=0.1.10
529 557 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
530 558 with older clients
531 559 - anonymous access, authentication via ldap
532 560 - performance upgrade for cached repos list - each repository has its own
533 561 cache that's invalidated when needed.
534 562 - performance upgrades on repositories with large amount of commits (20K+)
535 563 - main page quick filter for filtering repositories
536 564 - user dashboards with ability to follow chosen repositories actions
537 565 - sends email to admin on new user registration
538 566 - added cache/statistics reset options into repository settings
539 567 - more detailed action logger (based on hooks) with pushed changesets lists
540 568 and options to disable those hooks from admin panel
541 569 - introduced new enhanced changelog for merges that shows more accurate results
542 570 - new improved and faster code stats (based on pygments lexers mapping tables,
543 571 showing up to 10 trending sources for each repository. Additionally stats
544 572 can be disabled in repository settings.
545 573 - gui optimizations, fixed application width to 1024px
546 574 - added cut off (for large files/changesets) limit into config files
547 575 - whoosh, celeryd, upgrade moved to paster command
548 576 - other than sqlite database backends can be used
549 577
550 578 fixes
551 579 +++++
552 580
553 581 - fixes #61 forked repo was showing only after cache expired
554 582 - fixes #76 no confirmation on user deletes
555 583 - fixes #66 Name field misspelled
556 584 - fixes #72 block user removal when he owns repositories
557 585 - fixes #69 added password confirmation fields
558 586 - fixes #87 RhodeCode crashes occasionally on updating repository owner
559 587 - fixes #82 broken annotations on files with more than 1 blank line at the end
560 588 - a lot of fixes and tweaks for file browser
561 589 - fixed detached session issues
562 590 - fixed when user had no repos he would see all repos listed in my account
563 591 - fixed ui() instance bug when global hgrc settings was loaded for server
564 592 instance and all hgrc options were merged with our db ui() object
565 593 - numerous small bugfixes
566 594
567 595 (special thanks for TkSoh for detailed feedback)
568 596
569 597
570 598 1.0.2 (**2010-11-12**)
571 599 ----------------------
572 600
573 601 news
574 602 ++++
575 603
576 604 - tested under python2.7
577 605 - bumped sqlalchemy and celery versions
578 606
579 607 fixes
580 608 +++++
581 609
582 610 - fixed #59 missing graph.js
583 611 - fixed repo_size crash when repository had broken symlinks
584 612 - fixed python2.5 crashes.
585 613
586 614
587 615 1.0.1 (**2010-11-10**)
588 616 ----------------------
589 617
590 618 news
591 619 ++++
592 620
593 621 - small css updated
594 622
595 623 fixes
596 624 +++++
597 625
598 626 - fixed #53 python2.5 incompatible enumerate calls
599 627 - fixed #52 disable mercurial extension for web
600 628 - fixed #51 deleting repositories don't delete it's dependent objects
601 629
602 630
603 631 1.0.0 (**2010-11-02**)
604 632 ----------------------
605 633
606 634 - security bugfix simplehg wasn't checking for permissions on commands
607 635 other than pull or push.
608 636 - fixed doubled messages after push or pull in admin journal
609 637 - templating and css corrections, fixed repo switcher on chrome, updated titles
610 638 - admin menu accessible from options menu on repository view
611 639 - permissions cached queries
612 640
613 641 1.0.0rc4 (**2010-10-12**)
614 642 --------------------------
615 643
616 644 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
617 645 - removed cache_manager settings from sqlalchemy meta
618 646 - added sqlalchemy cache settings to ini files
619 647 - validated password length and added second try of failure on paster setup-app
620 648 - fixed setup database destroy prompt even when there was no db
621 649
622 650
623 651 1.0.0rc3 (**2010-10-11**)
624 652 -------------------------
625 653
626 654 - fixed i18n during installation.
627 655
628 656 1.0.0rc2 (**2010-10-11**)
629 657 -------------------------
630 658
631 659 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
632 660 occure. After vcs is fixed it'll be put back again.
633 661 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,94 +1,97 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.__init__
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode, a web based repository management based on pylons
7 7 versioning implementation: http://www.python.org/dev/peps/pep-0386/
8 8
9 9 :created_on: Apr 9, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import sys
27 27 import platform
28 28
29 29 VERSION = (1, 4, 0, 'b')
30 30
31 31 try:
32 32 from rhodecode.lib import get_current_revision
33 33 _rev = get_current_revision()
34 34 if _rev and len(VERSION) > 3:
35 35 VERSION += ('dev%s' % _rev[0],)
36 36 except ImportError:
37 37 pass
38 38
39 39 __version__ = ('.'.join((str(each) for each in VERSION[:3])) +
40 40 '.'.join(VERSION[3:]))
41 41 __dbversion__ = 6 # defines current db version for migrations
42 42 __platform__ = platform.system()
43 43 __license__ = 'GPLv3'
44 44 __py_version__ = sys.version_info
45 45
46 46 PLATFORM_WIN = ('Windows')
47 47 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
48 48
49 is_windows = __platform__ in PLATFORM_WIN
50 is_unix = __platform__ in PLATFORM_OTHERS
51
49 52 requirements = [
50 53 "Pylons==1.0.0",
51 54 "Beaker==1.6.3",
52 55 "WebHelpers==1.3",
53 56 "formencode==1.2.4",
54 57 "SQLAlchemy==0.7.6",
55 58 "Mako==0.7.0",
56 59 "pygments>=1.4",
57 60 "whoosh>=2.4.0,<2.5",
58 61 "celery>=2.2.5,<2.3",
59 62 "babel",
60 63 "python-dateutil>=1.5.0,<2.0.0",
61 64 "dulwich>=0.8.5,<0.9.0",
62 65 "webob==1.0.8",
63 66 "markdown==2.1.1",
64 67 "docutils==0.8.1",
68 "simplejson==2.5.2",
65 69 ]
66 70
67 71 if __py_version__ < (2, 6):
68 requirements.append("simplejson")
69 72 requirements.append("pysqlite")
70 73
71 if __platform__ in PLATFORM_WIN:
74 if is_windows:
72 75 requirements.append("mercurial>=2.2.1,<2.3")
73 76 else:
74 77 requirements.append("py-bcrypt")
75 78 requirements.append("mercurial>=2.2.1,<2.3")
76 79
77 80
78 81 def get_version():
79 82 """Returns shorter version (digit parts only) as string."""
80 83
81 84 return '.'.join((str(each) for each in VERSION[:3]))
82 85
83 86 BACKENDS = {
84 87 'hg': 'Mercurial repository',
85 88 'git': 'Git repository',
86 89 }
87 90
88 91 CELERY_ON = False
89 92
90 93 # link to config for pylons
91 94 CONFIG = {}
92 95
93 96 # Linked module for extensions
94 97 EXTENSIONS = {}
@@ -1,79 +1,81 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.config.rcextensions.make_rcextensions
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Whoosh indexing module for RhodeCode
7 7
8 8 :created_on: Mar 6, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 from __future__ import with_statement
26
25 27 import os
26 28 import sys
27 29 import pkg_resources
28 30 import traceback
29 31 import logging
30 32 from os.path import dirname as dn, join as jn
31 33
32 34 #to get the rhodecode import
33 35 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
34 36
35 37 from rhodecode.lib.utils import BasePasterCommand, Command, ask_ok
36 38
37 39 log = logging.getLogger(__name__)
38 40
39 41
40 42 class MakeRcExt(BasePasterCommand):
41 43
42 44 max_args = 1
43 45 min_args = 1
44 46
45 47 usage = "CONFIG_FILE"
46 48 summary = "Creates additional extensions for rhodecode"
47 49 group_name = "RhodeCode"
48 50 takes_config_file = -1
49 51 parser = Command.standard_parser(verbose=True)
50 52
51 53 def command(self):
52 54 logging.config.fileConfig(self.path_to_ini_file)
53 55 from pylons import config
54 56
55 57 def _make_file(ext_file):
56 58 bdir = os.path.split(ext_file)[0]
57 59 if not os.path.isdir(bdir):
58 60 os.makedirs(bdir)
59 61 with open(ext_file, 'wb') as f:
60 62 f.write(tmpl)
61 63 log.info('Writen new extensions file to %s' % ext_file)
62 64
63 65 here = config['here']
64 66 tmpl = pkg_resources.resource_string(
65 67 'rhodecode', jn('config', 'rcextensions', '__init__.py')
66 68 )
67 69 ext_file = jn(here, 'rcextensions', '__init__.py')
68 70 if os.path.exists(ext_file):
69 71 msg = ('Extension file already exists, do you want '
70 72 'to overwrite it ? [y/n]')
71 73 if ask_ok(msg):
72 74 _make_file(ext_file)
73 75 else:
74 76 log.info('nothing done...')
75 77 else:
76 78 _make_file(ext_file)
77 79
78 80 def update_parser(self):
79 81 pass
@@ -1,486 +1,486 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import traceback
29 import tempfile
29 30
30 31 from pylons import request, response, tmpl_context as c, url
31 32 from pylons.i18n.translation import _
32 33 from pylons.controllers.util import redirect
33 34 from pylons.decorators import jsonify
34 35
35 36 from rhodecode.lib import diffs
36 37 from rhodecode.lib import helpers as h
37 38
38 39 from rhodecode.lib.compat import OrderedDict
39 40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
40 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 42 from rhodecode.lib.base import BaseRepoController, render
42 43 from rhodecode.lib.utils import EmptyChangeset
43 44 from rhodecode.lib.vcs.conf import settings
44 45 from rhodecode.lib.vcs.exceptions import RepositoryError, \
45 46 ChangesetDoesNotExistError, EmptyRepositoryError, \
46 47 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
47 48 from rhodecode.lib.vcs.nodes import FileNode
48 49
49 50 from rhodecode.model.repo import RepoModel
50 51 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.db import Repository
51 53
52 54 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
53 55 _context_url, get_line_ctx, get_ignore_ws
54 56
55 57
56 58 log = logging.getLogger(__name__)
57 59
58 60
59 61 class FilesController(BaseRepoController):
60 62
61 63 @LoginRequired()
62 64 def __before__(self):
63 65 super(FilesController, self).__before__()
64 66 c.cut_off_limit = self.cut_off_limit
65 67
66 68 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
67 69 """
68 70 Safe way to get changeset if error occur it redirects to tip with
69 71 proper message
70 72
71 73 :param rev: revision to fetch
72 74 :param repo_name: repo name to redirect after
73 75 """
74 76
75 77 try:
76 78 return c.rhodecode_repo.get_changeset(rev)
77 79 except EmptyRepositoryError, e:
78 80 if not redirect_after:
79 81 return None
80 82 url_ = url('files_add_home',
81 83 repo_name=c.repo_name,
82 84 revision=0, f_path='')
83 85 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
84 86 h.flash(h.literal(_('There are no files yet %s' % add_new)),
85 87 category='warning')
86 88 redirect(h.url('summary_home', repo_name=repo_name))
87 89
88 90 except RepositoryError, e:
89 91 h.flash(str(e), category='warning')
90 92 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
91 93
92 94 def __get_filenode_or_redirect(self, repo_name, cs, path):
93 95 """
94 96 Returns file_node, if error occurs or given path is directory,
95 97 it'll redirect to top level path
96 98
97 99 :param repo_name: repo_name
98 100 :param cs: given changeset
99 101 :param path: path to lookup
100 102 """
101 103
102 104 try:
103 105 file_node = cs.get_node(path)
104 106 if file_node.is_dir():
105 107 raise RepositoryError('given path is a directory')
106 108 except RepositoryError, e:
107 109 h.flash(str(e), category='warning')
108 110 redirect(h.url('files_home', repo_name=repo_name,
109 111 revision=cs.raw_id))
110 112
111 113 return file_node
112 114
113 115 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
114 116 'repository.admin')
115 117 def index(self, repo_name, revision, f_path, annotate=False):
116 118 # redirect to given revision from form if given
117 119 post_revision = request.POST.get('at_rev', None)
118 120 if post_revision:
119 121 cs = self.__get_cs_or_redirect(post_revision, repo_name)
120 122 redirect(url('files_home', repo_name=c.repo_name,
121 123 revision=cs.raw_id, f_path=f_path))
122 124
123 125 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
124 126 c.branch = request.GET.get('branch', None)
125 127 c.f_path = f_path
126 128 c.annotate = annotate
127 129 cur_rev = c.changeset.revision
128 130
129 131 # prev link
130 132 try:
131 133 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
132 134 c.url_prev = url('files_home', repo_name=c.repo_name,
133 135 revision=prev_rev.raw_id, f_path=f_path)
134 136 if c.branch:
135 137 c.url_prev += '?branch=%s' % c.branch
136 138 except (ChangesetDoesNotExistError, VCSError):
137 139 c.url_prev = '#'
138 140
139 141 # next link
140 142 try:
141 143 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
142 144 c.url_next = url('files_home', repo_name=c.repo_name,
143 145 revision=next_rev.raw_id, f_path=f_path)
144 146 if c.branch:
145 147 c.url_next += '?branch=%s' % c.branch
146 148 except (ChangesetDoesNotExistError, VCSError):
147 149 c.url_next = '#'
148 150
149 151 # files or dirs
150 152 try:
151 153 c.file = c.changeset.get_node(f_path)
152 154
153 155 if c.file.is_file():
154 156 c.file_history = self._get_node_history(c.changeset, f_path)
155 157 else:
156 158 c.file_history = []
157 159 except RepositoryError, e:
158 160 h.flash(str(e), category='warning')
159 161 redirect(h.url('files_home', repo_name=repo_name,
160 162 revision=revision))
161 163
162 164 return render('files/files.html')
163 165
164 166 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
165 167 'repository.admin')
166 168 def rawfile(self, repo_name, revision, f_path):
167 169 cs = self.__get_cs_or_redirect(revision, repo_name)
168 170 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
169 171
170 172 response.content_disposition = 'attachment; filename=%s' % \
171 safe_str(f_path.split(os.sep)[-1])
173 safe_str(f_path.split(Repository.url_sep())[-1])
172 174
173 175 response.content_type = file_node.mimetype
174 176 return file_node.content
175 177
176 178 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
177 179 'repository.admin')
178 180 def raw(self, repo_name, revision, f_path):
179 181 cs = self.__get_cs_or_redirect(revision, repo_name)
180 182 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
181 183
182 184 raw_mimetype_mapping = {
183 185 # map original mimetype to a mimetype used for "show as raw"
184 186 # you can also provide a content-disposition to override the
185 187 # default "attachment" disposition.
186 188 # orig_type: (new_type, new_dispo)
187 189
188 190 # show images inline:
189 191 'image/x-icon': ('image/x-icon', 'inline'),
190 192 'image/png': ('image/png', 'inline'),
191 193 'image/gif': ('image/gif', 'inline'),
192 194 'image/jpeg': ('image/jpeg', 'inline'),
193 195 'image/svg+xml': ('image/svg+xml', 'inline'),
194 196 }
195 197
196 198 mimetype = file_node.mimetype
197 199 try:
198 200 mimetype, dispo = raw_mimetype_mapping[mimetype]
199 201 except KeyError:
200 202 # we don't know anything special about this, handle it safely
201 203 if file_node.is_binary:
202 204 # do same as download raw for binary files
203 205 mimetype, dispo = 'application/octet-stream', 'attachment'
204 206 else:
205 207 # do not just use the original mimetype, but force text/plain,
206 208 # otherwise it would serve text/html and that might be unsafe.
207 209 # Note: underlying vcs library fakes text/plain mimetype if the
208 210 # mimetype can not be determined and it thinks it is not
209 211 # binary.This might lead to erroneous text display in some
210 212 # cases, but helps in other cases, like with text files
211 213 # without extension.
212 214 mimetype, dispo = 'text/plain', 'inline'
213 215
214 216 if dispo == 'attachment':
215 217 dispo = 'attachment; filename=%s' % \
216 218 safe_str(f_path.split(os.sep)[-1])
217 219
218 220 response.content_disposition = dispo
219 221 response.content_type = mimetype
220 222 return file_node.content
221 223
222 224 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
223 225 def edit(self, repo_name, revision, f_path):
224 226 r_post = request.POST
225 227
226 228 c.cs = self.__get_cs_or_redirect(revision, repo_name)
227 229 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
228 230
229 231 if c.file.is_binary:
230 232 return redirect(url('files_home', repo_name=c.repo_name,
231 233 revision=c.cs.raw_id, f_path=f_path))
232 234
233 235 c.f_path = f_path
234 236
235 237 if r_post:
236 238
237 239 old_content = c.file.content
238 240 sl = old_content.splitlines(1)
239 241 first_line = sl[0] if sl else ''
240 242 # modes: 0 - Unix, 1 - Mac, 2 - DOS
241 243 mode = detect_mode(first_line, 0)
242 244 content = convert_line_endings(r_post.get('content'), mode)
243 245
244 246 message = r_post.get('message') or (_('Edited %s via RhodeCode')
245 247 % (f_path))
246 248 author = self.rhodecode_user.full_contact
247 249
248 250 if content == old_content:
249 251 h.flash(_('No changes'),
250 252 category='warning')
251 253 return redirect(url('changeset_home', repo_name=c.repo_name,
252 254 revision='tip'))
253 255
254 256 try:
255 257 self.scm_model.commit_change(repo=c.rhodecode_repo,
256 258 repo_name=repo_name, cs=c.cs,
257 259 user=self.rhodecode_user,
258 260 author=author, message=message,
259 261 content=content, f_path=f_path)
260 262 h.flash(_('Successfully committed to %s' % f_path),
261 263 category='success')
262 264
263 265 except Exception:
264 266 log.error(traceback.format_exc())
265 267 h.flash(_('Error occurred during commit'), category='error')
266 268 return redirect(url('changeset_home',
267 269 repo_name=c.repo_name, revision='tip'))
268 270
269 271 return render('files/files_edit.html')
270 272
271 273 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
272 274 def add(self, repo_name, revision, f_path):
273 275 r_post = request.POST
274 276 c.cs = self.__get_cs_or_redirect(revision, repo_name,
275 277 redirect_after=False)
276 278 if c.cs is None:
277 279 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
278 280
279 281 c.f_path = f_path
280 282
281 283 if r_post:
282 284 unix_mode = 0
283 285 content = convert_line_endings(r_post.get('content'), unix_mode)
284 286
285 287 message = r_post.get('message') or (_('Added %s via RhodeCode')
286 288 % (f_path))
287 289 location = r_post.get('location')
288 290 filename = r_post.get('filename')
289 291 file_obj = r_post.get('upload_file', None)
290 292
291 293 if file_obj is not None and hasattr(file_obj, 'filename'):
292 294 filename = file_obj.filename
293 295 content = file_obj.file
294 296
295 297 node_path = os.path.join(location, filename)
296 298 author = self.rhodecode_user.full_contact
297 299
298 300 if not content:
299 301 h.flash(_('No content'), category='warning')
300 302 return redirect(url('changeset_home', repo_name=c.repo_name,
301 303 revision='tip'))
302 304 if not filename:
303 305 h.flash(_('No filename'), category='warning')
304 306 return redirect(url('changeset_home', repo_name=c.repo_name,
305 307 revision='tip'))
306 308
307 309 try:
308 310 self.scm_model.create_node(repo=c.rhodecode_repo,
309 311 repo_name=repo_name, cs=c.cs,
310 312 user=self.rhodecode_user,
311 313 author=author, message=message,
312 314 content=content, f_path=node_path)
313 315 h.flash(_('Successfully committed to %s' % node_path),
314 316 category='success')
315 317 except NodeAlreadyExistsError, e:
316 318 h.flash(_(e), category='error')
317 319 except Exception:
318 320 log.error(traceback.format_exc())
319 321 h.flash(_('Error occurred during commit'), category='error')
320 322 return redirect(url('changeset_home',
321 323 repo_name=c.repo_name, revision='tip'))
322 324
323 325 return render('files/files_add.html')
324 326
325 327 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 328 'repository.admin')
327 329 def archivefile(self, repo_name, fname):
328 330
329 331 fileformat = None
330 332 revision = None
331 333 ext = None
332 334 subrepos = request.GET.get('subrepos') == 'true'
333 335
334 336 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
335 337 archive_spec = fname.split(ext_data[1])
336 338 if len(archive_spec) == 2 and archive_spec[1] == '':
337 339 fileformat = a_type or ext_data[1]
338 340 revision = archive_spec[0]
339 341 ext = ext_data[1]
340 342
341 343 try:
342 344 dbrepo = RepoModel().get_by_repo_name(repo_name)
343 345 if dbrepo.enable_downloads is False:
344 346 return _('downloads disabled')
345 347
346 348 if c.rhodecode_repo.alias == 'hg':
347 349 # patch and reset hooks section of UI config to not run any
348 350 # hooks on fetching archives with subrepos
349 351 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
350 352 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
351 353
352 354 cs = c.rhodecode_repo.get_changeset(revision)
353 355 content_type = settings.ARCHIVE_SPECS[fileformat][0]
354 356 except ChangesetDoesNotExistError:
355 357 return _('Unknown revision %s') % revision
356 358 except EmptyRepositoryError:
357 359 return _('Empty repository')
358 360 except (ImproperArchiveTypeError, KeyError):
359 361 return _('Unknown archive type')
360 362
363 archive = tempfile.NamedTemporaryFile(mode='w+r+b', delete=False)
364 cs.fill_archive(stream=archive, kind=fileformat, subrepos=subrepos)
365 archive.close()
361 366 response.content_type = content_type
362 367 response.content_disposition = 'attachment; filename=%s-%s%s' \
363 % (repo_name, revision, ext)
364
365 import tempfile
366 archive = tempfile.mkstemp()[1]
367 t = open(archive, 'wb')
368 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
368 % (repo_name, revision[:12], ext)
369 response.content_length = str(os.path.getsize(archive.name))
369 370
370 def get_chunked_archive(archive):
371 stream = open(archive, 'rb')
371 def get_chunked_archive(tmpfile):
372 372 while True:
373 data = stream.read(4096)
373 data = tmpfile.read(16 * 1024)
374 374 if not data:
375 os.remove(archive)
375 tmpfile.close()
376 os.unlink(tmpfile.name)
376 377 break
377 378 yield data
378
379 return get_chunked_archive(archive)
379 return get_chunked_archive(tmpfile=open(archive.name,'rb'))
380 380
381 381 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
382 382 'repository.admin')
383 383 def diff(self, repo_name, f_path):
384 384 ignore_whitespace = request.GET.get('ignorews') == '1'
385 385 line_context = request.GET.get('context', 3)
386 386 diff1 = request.GET.get('diff1', '')
387 387 diff2 = request.GET.get('diff2', '')
388 388 c.action = request.GET.get('diff')
389 389 c.no_changes = diff1 == diff2
390 390 c.f_path = f_path
391 391 c.big_diff = False
392 392 c.anchor_url = anchor_url
393 393 c.ignorews_url = _ignorews_url
394 394 c.context_url = _context_url
395 395 c.changes = OrderedDict()
396 396 c.changes[diff2] = []
397 397 try:
398 398 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
399 399 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
400 400 node1 = c.changeset_1.get_node(f_path)
401 401 else:
402 402 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
403 403 node1 = FileNode('.', '', changeset=c.changeset_1)
404 404
405 405 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
406 406 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
407 407 node2 = c.changeset_2.get_node(f_path)
408 408 else:
409 409 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
410 410 node2 = FileNode('.', '', changeset=c.changeset_2)
411 411 except RepositoryError:
412 412 return redirect(url('files_home', repo_name=c.repo_name,
413 413 f_path=f_path))
414 414
415 415 if c.action == 'download':
416 416 _diff = diffs.get_gitdiff(node1, node2,
417 417 ignore_whitespace=ignore_whitespace,
418 418 context=line_context)
419 419 diff = diffs.DiffProcessor(_diff, format='gitdiff')
420 420
421 421 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
422 422 response.content_type = 'text/plain'
423 423 response.content_disposition = (
424 424 'attachment; filename=%s' % diff_name
425 425 )
426 426 return diff.raw_diff()
427 427
428 428 elif c.action == 'raw':
429 429 _diff = diffs.get_gitdiff(node1, node2,
430 430 ignore_whitespace=ignore_whitespace,
431 431 context=line_context)
432 432 diff = diffs.DiffProcessor(_diff, format='gitdiff')
433 433 response.content_type = 'text/plain'
434 434 return diff.raw_diff()
435 435
436 436 else:
437 437 fid = h.FID(diff2, node2.path)
438 438 line_context_lcl = get_line_ctx(fid, request.GET)
439 439 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
440 440
441 441 lim = request.GET.get('fulldiff') or self.cut_off_limit
442 442 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
443 443 filenode_new=node2,
444 444 cut_off_limit=lim,
445 445 ignore_whitespace=ign_whitespace_lcl,
446 446 line_context=line_context_lcl,
447 447 enable_comments=False)
448 448
449 449 c.changes = [('', node2, diff, cs1, cs2, st,)]
450 450
451 451 return render('files/file_diff.html')
452 452
453 453 def _get_node_history(self, cs, f_path):
454 454 changesets = cs.get_file_history(f_path)
455 455 hist_l = []
456 456
457 457 changesets_group = ([], _("Changesets"))
458 458 branches_group = ([], _("Branches"))
459 459 tags_group = ([], _("Tags"))
460 460 _hg = cs.repository.alias == 'hg'
461 461 for chs in changesets:
462 462 _branch = '(%s)' % chs.branch if _hg else ''
463 463 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
464 464 changesets_group[0].append((chs.raw_id, n_desc,))
465 465
466 466 hist_l.append(changesets_group)
467 467
468 468 for name, chs in c.rhodecode_repo.branches.items():
469 469 branches_group[0].append((chs, name),)
470 470 hist_l.append(branches_group)
471 471
472 472 for name, chs in c.rhodecode_repo.tags.items():
473 473 tags_group[0].append((chs, name),)
474 474 hist_l.append(tags_group)
475 475
476 476 return hist_l
477 477
478 478 @jsonify
479 479 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
480 480 'repository.admin')
481 481 def nodelist(self, repo_name, revision, f_path):
482 482 if request.environ.get('HTTP_X_PARTIAL_XHR'):
483 483 cs = self.__get_cs_or_redirect(revision, repo_name)
484 484 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
485 485 flat=False)
486 486 return _d + _f
@@ -1,237 +1,240 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.summary
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Summary controller for Rhodecode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import traceback
27 27 import calendar
28 28 import logging
29 29 import urllib
30 30 from time import mktime
31 31 from datetime import timedelta, date
32 32 from urlparse import urlparse
33 33 from rhodecode.lib.compat import product
34 34
35 35 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
36 36 NodeDoesNotExistError
37 37
38 38 from pylons import tmpl_context as c, request, url, config
39 39 from pylons.i18n.translation import _
40 40
41 41 from beaker.cache import cache_region, region_invalidate
42 42
43 43 from rhodecode.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
44 44 from rhodecode.model.db import Statistics, CacheInvalidation
45 45 from rhodecode.lib.utils2 import safe_unicode
46 46 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
47 47 from rhodecode.lib.base import BaseRepoController, render
48 48 from rhodecode.lib.utils import EmptyChangeset
49 49 from rhodecode.lib.markup_renderer import MarkupRenderer
50 50 from rhodecode.lib.celerylib import run_task
51 51 from rhodecode.lib.celerylib.tasks import get_commits_stats
52 52 from rhodecode.lib.helpers import RepoPage
53 53 from rhodecode.lib.compat import json, OrderedDict
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
58 58 sorted(list(product(ALL_READMES, ALL_EXTS)),
59 59 key=lambda y:y[0][1] + y[1][1])]
60 60
61 61
62 62 class SummaryController(BaseRepoController):
63 63
64 64 @LoginRequired()
65 65 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
66 66 'repository.admin')
67 67 def __before__(self):
68 68 super(SummaryController, self).__before__()
69 69
70 70 def index(self, repo_name):
71 71 c.dbrepo = dbrepo = c.rhodecode_db_repo
72 72 c.following = self.scm_model.is_following_repo(repo_name,
73 73 self.rhodecode_user.user_id)
74 74
75 75 def url_generator(**kw):
76 76 return url('shortlog_home', repo_name=repo_name, size=10, **kw)
77 77
78 78 c.repo_changesets = RepoPage(c.rhodecode_repo, page=1,
79 79 items_per_page=10, url=url_generator)
80 80
81 81 if self.rhodecode_user.username == 'default':
82 82 # for default(anonymous) user we don't need to pass credentials
83 83 username = ''
84 84 password = ''
85 85 else:
86 86 username = str(self.rhodecode_user.username)
87 87 password = '@'
88 88
89 89 parsed_url = urlparse(url.current(qualified=True))
90 90
91 91 default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
92 92
93 93 uri_tmpl = config.get('clone_uri', default_clone_uri)
94 94 uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
95 95 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
96 96 uri_dict = {
97 97 'user': username,
98 98 'pass': password,
99 99 'scheme': parsed_url.scheme,
100 100 'netloc': parsed_url.netloc,
101 101 'path': decoded_path
102 102 }
103 103
104 104 uri = uri_tmpl % uri_dict
105 105 # generate another clone url by id
106 106 uri_dict.update(
107 107 {'path': decoded_path.replace(repo_name, '_%s' % c.dbrepo.repo_id)}
108 108 )
109 109 uri_id = uri_tmpl % uri_dict
110 110
111 111 c.clone_repo_url = uri
112 112 c.clone_repo_url_id = uri_id
113 113 c.repo_tags = OrderedDict()
114 114 for name, hash_ in c.rhodecode_repo.tags.items()[:10]:
115 115 try:
116 116 c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash_)
117 117 except ChangesetError:
118 118 c.repo_tags[name] = EmptyChangeset(hash_)
119 119
120 120 c.repo_branches = OrderedDict()
121 121 for name, hash_ in c.rhodecode_repo.branches.items()[:10]:
122 122 try:
123 123 c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash_)
124 124 except ChangesetError:
125 125 c.repo_branches[name] = EmptyChangeset(hash_)
126 126
127 127 td = date.today() + timedelta(days=1)
128 128 td_1m = td - timedelta(days=calendar.mdays[td.month])
129 129 td_1y = td - timedelta(days=365)
130 130
131 131 ts_min_m = mktime(td_1m.timetuple())
132 132 ts_min_y = mktime(td_1y.timetuple())
133 133 ts_max_y = mktime(td.timetuple())
134 134
135 135 if dbrepo.enable_statistics:
136 136 c.show_stats = True
137 137 c.no_data_msg = _('No data loaded yet')
138 138 run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y, ts_max_y)
139 139 else:
140 140 c.show_stats = False
141 141 c.no_data_msg = _('Statistics are disabled for this repository')
142 142 c.ts_min = ts_min_m
143 143 c.ts_max = ts_max_y
144 144
145 145 stats = self.sa.query(Statistics)\
146 146 .filter(Statistics.repository == dbrepo)\
147 147 .scalar()
148 148
149 149 c.stats_percentage = 0
150 150
151 151 if stats and stats.languages:
152 152 c.no_data = False is dbrepo.enable_statistics
153 153 lang_stats_d = json.loads(stats.languages)
154 154 c.commit_data = stats.commit_activity
155 155 c.overview_data = stats.commit_activity_combined
156 156
157 157 lang_stats = ((x, {"count": y,
158 158 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
159 159 for x, y in lang_stats_d.items())
160 160
161 161 c.trending_languages = json.dumps(
162 162 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
163 163 )
164 164 last_rev = stats.stat_on_revision + 1
165 165 c.repo_last_rev = c.rhodecode_repo.count()\
166 166 if c.rhodecode_repo.revisions else 0
167 167 if last_rev == 0 or c.repo_last_rev == 0:
168 168 pass
169 169 else:
170 170 c.stats_percentage = '%.2f' % ((float((last_rev)) /
171 171 c.repo_last_rev) * 100)
172 172 else:
173 173 c.commit_data = json.dumps({})
174 174 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
175 175 c.trending_languages = json.dumps({})
176 176 c.no_data = True
177 177
178 178 c.enable_downloads = dbrepo.enable_downloads
179 179 if c.enable_downloads:
180 180 c.download_options = self._get_download_links(c.rhodecode_repo)
181 181
182 c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_db_repo)
182 c.readme_data, c.readme_file = self.__get_readme_data(
183 c.rhodecode_db_repo.repo_name, c.rhodecode_repo
184 )
183 185 return render('summary/summary.html')
184 186
185 def __get_readme_data(self, repo):
187 def __get_readme_data(self, repo_name, repo):
186 188
187 189 @cache_region('long_term')
188 190 def _get_readme_from_cache(key):
189 191 readme_data = None
190 192 readme_file = None
191 193 log.debug('Fetching readme file')
192 194 try:
193 cs = repo.get_changeset('tip')
195 cs = repo.get_changeset() # fetches TIP
194 196 renderer = MarkupRenderer()
195 197 for f in README_FILES:
196 198 try:
197 199 readme = cs.get_node(f)
198 200 readme_file = f
199 201 readme_data = renderer.render(readme.content, f)
200 202 log.debug('Found readme %s' % readme_file)
201 203 break
202 204 except NodeDoesNotExistError:
203 205 continue
204 206 except ChangesetError:
207 log.error(traceback.format_exc())
205 208 pass
206 209 except EmptyRepositoryError:
207 210 pass
208 211 except Exception:
209 212 log.error(traceback.format_exc())
210 213
211 214 return readme_data, readme_file
212 215
213 key = repo.repo_name + '_README'
216 key = repo_name + '_README'
214 217 inv = CacheInvalidation.invalidate(key)
215 218 if inv is not None:
216 219 region_invalidate(_get_readme_from_cache, None, key)
217 220 CacheInvalidation.set_valid(inv.cache_key)
218 221 return _get_readme_from_cache(key)
219 222
220 223 def _get_download_links(self, repo):
221 224
222 225 download_l = []
223 226
224 227 branches_group = ([], _("Branches"))
225 228 tags_group = ([], _("Tags"))
226 229
227 230 for name, chs in c.rhodecode_repo.branches.items():
228 231 #chs = chs.split(':')[-1]
229 232 branches_group[0].append((chs, name),)
230 233 download_l.append(branches_group)
231 234
232 235 for name, chs in c.rhodecode_repo.tags.items():
233 236 #chs = chs.split(':')[-1]
234 237 tags_group[0].append((chs, name),)
235 238 download_l.append(tags_group)
236 239
237 240 return download_l
@@ -1,821 +1,821 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import random
27 27 import logging
28 28 import traceback
29 29 import hashlib
30 30
31 31 from tempfile import _RandomNameSequence
32 32 from decorator import decorator
33 33
34 34 from pylons import config, url, request
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 39 from rhodecode.model.meta import Session
40 40
41 41 if __platform__ in PLATFORM_WIN:
42 42 from hashlib import sha256
43 43 if __platform__ in PLATFORM_OTHERS:
44 44 import bcrypt
45 45
46 46 from rhodecode.lib.utils2 import str2bool, safe_unicode
47 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
48 48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
49 49 from rhodecode.lib.auth_ldap import AuthLdap
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class PasswordGenerator(object):
59 59 """
60 60 This is a simple class for generating password from different sets of
61 61 characters
62 62 usage::
63 63
64 64 passwd_gen = PasswordGenerator()
65 65 #print 8-letter password containing only big and small letters
66 66 of alphabet
67 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
67 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
68 68 """
69 69 ALPHABETS_NUM = r'''1234567890'''
70 70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
71 71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
72 72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
73 73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
74 74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
75 75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
76 76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
77 77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
78 78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
79 79
80 80 def __init__(self, passwd=''):
81 81 self.passwd = passwd
82 82
83 83 def gen_password(self, length, type_=None):
84 84 if type_ is None:
85 85 type_ = self.ALPHABETS_FULL
86 86 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
87 87 return self.passwd
88 88
89 89
90 90 class RhodeCodeCrypto(object):
91 91
92 92 @classmethod
93 93 def hash_string(cls, str_):
94 94 """
95 95 Cryptographic function used for password hashing based on pybcrypt
96 96 or pycrypto in windows
97 97
98 98 :param password: password to hash
99 99 """
100 100 if __platform__ in PLATFORM_WIN:
101 101 return sha256(str_).hexdigest()
102 102 elif __platform__ in PLATFORM_OTHERS:
103 103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
104 104 else:
105 105 raise Exception('Unknown or unsupported platform %s' \
106 106 % __platform__)
107 107
108 108 @classmethod
109 109 def hash_check(cls, password, hashed):
110 110 """
111 111 Checks matching password with it's hashed value, runs different
112 112 implementation based on platform it runs on
113 113
114 114 :param password: password
115 115 :param hashed: password in hashed form
116 116 """
117 117
118 118 if __platform__ in PLATFORM_WIN:
119 119 return sha256(password).hexdigest() == hashed
120 120 elif __platform__ in PLATFORM_OTHERS:
121 121 return bcrypt.hashpw(password, hashed) == hashed
122 122 else:
123 123 raise Exception('Unknown or unsupported platform %s' \
124 124 % __platform__)
125 125
126 126
127 127 def get_crypt_password(password):
128 128 return RhodeCodeCrypto.hash_string(password)
129 129
130 130
131 131 def check_password(password, hashed):
132 132 return RhodeCodeCrypto.hash_check(password, hashed)
133 133
134 134
135 135 def generate_api_key(str_, salt=None):
136 136 """
137 137 Generates API KEY from given string
138 138
139 139 :param str_:
140 140 :param salt:
141 141 """
142 142
143 143 if salt is None:
144 144 salt = _RandomNameSequence().next()
145 145
146 146 return hashlib.sha1(str_ + salt).hexdigest()
147 147
148 148
149 149 def authfunc(environ, username, password):
150 150 """
151 151 Dummy authentication wrapper function used in Mercurial and Git for
152 152 access control.
153 153
154 154 :param environ: needed only for using in Basic auth
155 155 """
156 156 return authenticate(username, password)
157 157
158 158
159 159 def authenticate(username, password):
160 160 """
161 161 Authentication function used for access control,
162 162 firstly checks for db authentication then if ldap is enabled for ldap
163 163 authentication, also creates ldap user if not in database
164 164
165 165 :param username: username
166 166 :param password: password
167 167 """
168 168
169 169 user_model = UserModel()
170 170 user = User.get_by_username(username)
171 171
172 172 log.debug('Authenticating user using RhodeCode account')
173 173 if user is not None and not user.ldap_dn:
174 174 if user.active:
175 175 if user.username == 'default' and user.active:
176 176 log.info('user %s authenticated correctly as anonymous user' %
177 177 username)
178 178 return True
179 179
180 180 elif user.username == username and check_password(password,
181 181 user.password):
182 182 log.info('user %s authenticated correctly' % username)
183 183 return True
184 184 else:
185 185 log.warning('user %s tried auth but is disabled' % username)
186 186
187 187 else:
188 188 log.debug('Regular authentication failed')
189 189 user_obj = User.get_by_username(username, case_insensitive=True)
190 190
191 191 if user_obj is not None and not user_obj.ldap_dn:
192 192 log.debug('this user already exists as non ldap')
193 193 return False
194 194
195 195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
196 196 #======================================================================
197 197 # FALLBACK TO LDAP AUTH IF ENABLE
198 198 #======================================================================
199 199 if str2bool(ldap_settings.get('ldap_active')):
200 200 log.debug("Authenticating user using ldap")
201 201 kwargs = {
202 202 'server': ldap_settings.get('ldap_host', ''),
203 203 'base_dn': ldap_settings.get('ldap_base_dn', ''),
204 204 'port': ldap_settings.get('ldap_port'),
205 205 'bind_dn': ldap_settings.get('ldap_dn_user'),
206 206 'bind_pass': ldap_settings.get('ldap_dn_pass'),
207 207 'tls_kind': ldap_settings.get('ldap_tls_kind'),
208 208 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
209 209 'ldap_filter': ldap_settings.get('ldap_filter'),
210 210 'search_scope': ldap_settings.get('ldap_search_scope'),
211 211 'attr_login': ldap_settings.get('ldap_attr_login'),
212 212 'ldap_version': 3,
213 213 }
214 214 log.debug('Checking for ldap authentication')
215 215 try:
216 216 aldap = AuthLdap(**kwargs)
217 217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
218 218 password)
219 219 log.debug('Got ldap DN response %s' % user_dn)
220 220
221 221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
222 222 .get(k), [''])[0]
223 223
224 224 user_attrs = {
225 225 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
226 226 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
227 227 'email': get_ldap_attr('ldap_attr_email'),
228 228 }
229 229
230 230 # don't store LDAP password since we don't need it. Override
231 231 # with some random generated password
232 232 _password = PasswordGenerator().gen_password(length=8)
233 233 # create this user on the fly if it doesn't exist in rhodecode
234 234 # database
235 235 if user_model.create_ldap(username, _password, user_dn,
236 236 user_attrs):
237 237 log.info('created new ldap user %s' % username)
238 238
239 239 Session.commit()
240 240 return True
241 241 except (LdapUsernameError, LdapPasswordError,):
242 242 pass
243 243 except (Exception,):
244 244 log.error(traceback.format_exc())
245 245 pass
246 246 return False
247 247
248 248
249 249 def login_container_auth(username):
250 250 user = User.get_by_username(username)
251 251 if user is None:
252 252 user_attrs = {
253 253 'name': username,
254 254 'lastname': None,
255 255 'email': None,
256 256 }
257 257 user = UserModel().create_for_container_auth(username, user_attrs)
258 258 if not user:
259 259 return None
260 260 log.info('User %s was created by container authentication' % username)
261 261
262 262 if not user.active:
263 263 return None
264 264
265 265 user.update_lastlogin()
266 266 Session.commit()
267 267
268 268 log.debug('User %s is now logged in by container authentication',
269 269 user.username)
270 270 return user
271 271
272 272
273 273 def get_container_username(environ, config):
274 274 username = None
275 275
276 276 if str2bool(config.get('container_auth_enabled', False)):
277 277 from paste.httpheaders import REMOTE_USER
278 278 username = REMOTE_USER(environ)
279 279
280 280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
281 281 username = environ.get('HTTP_X_FORWARDED_USER')
282 282
283 283 if username:
284 284 # Removing realm and domain from username
285 285 username = username.partition('@')[0]
286 286 username = username.rpartition('\\')[2]
287 287 log.debug('Received username %s from container' % username)
288 288
289 289 return username
290 290
291 291
292 292 class CookieStoreWrapper(object):
293 293
294 294 def __init__(self, cookie_store):
295 295 self.cookie_store = cookie_store
296 296
297 297 def __repr__(self):
298 298 return 'CookieStore<%s>' % (self.cookie_store)
299 299
300 300 def get(self, key, other=None):
301 301 if isinstance(self.cookie_store, dict):
302 302 return self.cookie_store.get(key, other)
303 303 elif isinstance(self.cookie_store, AuthUser):
304 304 return self.cookie_store.__dict__.get(key, other)
305 305
306 306
307 307 class AuthUser(object):
308 308 """
309 309 A simple object that handles all attributes of user in RhodeCode
310 310
311 311 It does lookup based on API key,given user, or user present in session
312 312 Then it fills all required information for such user. It also checks if
313 313 anonymous access is enabled and if so, it returns default user as logged
314 314 in
315 315 """
316 316
317 317 def __init__(self, user_id=None, api_key=None, username=None):
318 318
319 319 self.user_id = user_id
320 320 self.api_key = None
321 321 self.username = username
322 322
323 323 self.name = ''
324 324 self.lastname = ''
325 325 self.email = ''
326 326 self.is_authenticated = False
327 327 self.admin = False
328 328 self.permissions = {}
329 329 self._api_key = api_key
330 330 self.propagate_data()
331 331 self._instance = None
332 332
333 333 def propagate_data(self):
334 334 user_model = UserModel()
335 335 self.anonymous_user = User.get_by_username('default', cache=True)
336 336 is_user_loaded = False
337 337
338 338 # try go get user by api key
339 339 if self._api_key and self._api_key != self.anonymous_user.api_key:
340 340 log.debug('Auth User lookup by API KEY %s' % self._api_key)
341 341 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
342 342 # lookup by userid
343 343 elif (self.user_id is not None and
344 344 self.user_id != self.anonymous_user.user_id):
345 345 log.debug('Auth User lookup by USER ID %s' % self.user_id)
346 346 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
347 347 # lookup by username
348 348 elif self.username and \
349 349 str2bool(config.get('container_auth_enabled', False)):
350 350
351 351 log.debug('Auth User lookup by USER NAME %s' % self.username)
352 352 dbuser = login_container_auth(self.username)
353 353 if dbuser is not None:
354 354 for k, v in dbuser.get_dict().items():
355 355 setattr(self, k, v)
356 356 self.set_authenticated()
357 357 is_user_loaded = True
358 358 else:
359 359 log.debug('No data in %s that could been used to log in' % self)
360 360
361 361 if not is_user_loaded:
362 362 # if we cannot authenticate user try anonymous
363 363 if self.anonymous_user.active is True:
364 364 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
365 365 # then we set this user is logged in
366 366 self.is_authenticated = True
367 367 else:
368 368 self.user_id = None
369 369 self.username = None
370 370 self.is_authenticated = False
371 371
372 372 if not self.username:
373 373 self.username = 'None'
374 374
375 375 log.debug('Auth User is now %s' % self)
376 376 user_model.fill_perms(self)
377 377
378 378 @property
379 379 def is_admin(self):
380 380 return self.admin
381 381
382 382 def __repr__(self):
383 383 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
384 384 self.is_authenticated)
385 385
386 386 def set_authenticated(self, authenticated=True):
387 387 if self.user_id != self.anonymous_user.user_id:
388 388 self.is_authenticated = authenticated
389 389
390 390 def get_cookie_store(self):
391 391 return {'username': self.username,
392 392 'user_id': self.user_id,
393 393 'is_authenticated': self.is_authenticated}
394 394
395 395 @classmethod
396 396 def from_cookie_store(cls, cookie_store):
397 397 """
398 398 Creates AuthUser from a cookie store
399 399
400 400 :param cls:
401 401 :param cookie_store:
402 402 """
403 403 user_id = cookie_store.get('user_id')
404 404 username = cookie_store.get('username')
405 405 api_key = cookie_store.get('api_key')
406 406 return AuthUser(user_id, api_key, username)
407 407
408 408
409 409 def set_available_permissions(config):
410 410 """
411 411 This function will propagate pylons globals with all available defined
412 412 permission given in db. We don't want to check each time from db for new
413 413 permissions since adding a new permission also requires application restart
414 414 ie. to decorate new views with the newly created permission
415 415
416 416 :param config: current pylons config instance
417 417
418 418 """
419 419 log.info('getting information about all available permissions')
420 420 try:
421 421 sa = meta.Session
422 422 all_perms = sa.query(Permission).all()
423 423 except Exception:
424 424 pass
425 425 finally:
426 426 meta.Session.remove()
427 427
428 428 config['available_permissions'] = [x.permission_name for x in all_perms]
429 429
430 430
431 431 #==============================================================================
432 432 # CHECK DECORATORS
433 433 #==============================================================================
434 434 class LoginRequired(object):
435 435 """
436 436 Must be logged in to execute this function else
437 437 redirect to login page
438 438
439 439 :param api_access: if enabled this checks only for valid auth token
440 440 and grants access based on valid token
441 441 """
442 442
443 443 def __init__(self, api_access=False):
444 444 self.api_access = api_access
445 445
446 446 def __call__(self, func):
447 447 return decorator(self.__wrapper, func)
448 448
449 449 def __wrapper(self, func, *fargs, **fkwargs):
450 450 cls = fargs[0]
451 451 user = cls.rhodecode_user
452 452
453 453 api_access_ok = False
454 454 if self.api_access:
455 455 log.debug('Checking API KEY access for %s' % cls)
456 456 if user.api_key == request.GET.get('api_key'):
457 457 api_access_ok = True
458 458 else:
459 459 log.debug("API KEY token not valid")
460 460 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
461 461 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
462 462 if user.is_authenticated or api_access_ok:
463 463 log.info('user %s is authenticated and granted access to %s' % (
464 464 user.username, loc)
465 465 )
466 466 return func(*fargs, **fkwargs)
467 467 else:
468 468 log.warn('user %s NOT authenticated on func: %s' % (
469 469 user, loc)
470 470 )
471 471 p = url.current()
472 472
473 473 log.debug('redirecting to login page with %s' % p)
474 474 return redirect(url('login_home', came_from=p))
475 475
476 476
477 477 class NotAnonymous(object):
478 478 """
479 479 Must be logged in to execute this function else
480 480 redirect to login page"""
481 481
482 482 def __call__(self, func):
483 483 return decorator(self.__wrapper, func)
484 484
485 485 def __wrapper(self, func, *fargs, **fkwargs):
486 486 cls = fargs[0]
487 487 self.user = cls.rhodecode_user
488 488
489 489 log.debug('Checking if user is not anonymous @%s' % cls)
490 490
491 491 anonymous = self.user.username == 'default'
492 492
493 493 if anonymous:
494 494 p = url.current()
495 495
496 496 import rhodecode.lib.helpers as h
497 497 h.flash(_('You need to be a registered user to '
498 498 'perform this action'),
499 499 category='warning')
500 500 return redirect(url('login_home', came_from=p))
501 501 else:
502 502 return func(*fargs, **fkwargs)
503 503
504 504
505 505 class PermsDecorator(object):
506 506 """Base class for controller decorators"""
507 507
508 508 def __init__(self, *required_perms):
509 509 available_perms = config['available_permissions']
510 510 for perm in required_perms:
511 511 if perm not in available_perms:
512 512 raise Exception("'%s' permission is not defined" % perm)
513 513 self.required_perms = set(required_perms)
514 514 self.user_perms = None
515 515
516 516 def __call__(self, func):
517 517 return decorator(self.__wrapper, func)
518 518
519 519 def __wrapper(self, func, *fargs, **fkwargs):
520 520 cls = fargs[0]
521 521 self.user = cls.rhodecode_user
522 522 self.user_perms = self.user.permissions
523 523 log.debug('checking %s permissions %s for %s %s',
524 524 self.__class__.__name__, self.required_perms, cls, self.user)
525 525
526 526 if self.check_permissions():
527 527 log.debug('Permission granted for %s %s' % (cls, self.user))
528 528 return func(*fargs, **fkwargs)
529 529
530 530 else:
531 531 log.debug('Permission denied for %s %s' % (cls, self.user))
532 532 anonymous = self.user.username == 'default'
533 533
534 534 if anonymous:
535 535 p = url.current()
536 536
537 537 import rhodecode.lib.helpers as h
538 538 h.flash(_('You need to be a signed in to '
539 539 'view this page'),
540 540 category='warning')
541 541 return redirect(url('login_home', came_from=p))
542 542
543 543 else:
544 544 # redirect with forbidden ret code
545 545 return abort(403)
546 546
547 547 def check_permissions(self):
548 548 """Dummy function for overriding"""
549 549 raise Exception('You have to write this function in child class')
550 550
551 551
552 552 class HasPermissionAllDecorator(PermsDecorator):
553 553 """
554 554 Checks for access permission for all given predicates. All of them
555 555 have to be meet in order to fulfill the request
556 556 """
557 557
558 558 def check_permissions(self):
559 559 if self.required_perms.issubset(self.user_perms.get('global')):
560 560 return True
561 561 return False
562 562
563 563
564 564 class HasPermissionAnyDecorator(PermsDecorator):
565 565 """
566 566 Checks for access permission for any of given predicates. In order to
567 567 fulfill the request any of predicates must be meet
568 568 """
569 569
570 570 def check_permissions(self):
571 571 if self.required_perms.intersection(self.user_perms.get('global')):
572 572 return True
573 573 return False
574 574
575 575
576 576 class HasRepoPermissionAllDecorator(PermsDecorator):
577 577 """
578 578 Checks for access permission for all given predicates for specific
579 579 repository. All of them have to be meet in order to fulfill the request
580 580 """
581 581
582 582 def check_permissions(self):
583 583 repo_name = get_repo_slug(request)
584 584 try:
585 585 user_perms = set([self.user_perms['repositories'][repo_name]])
586 586 except KeyError:
587 587 return False
588 588 if self.required_perms.issubset(user_perms):
589 589 return True
590 590 return False
591 591
592 592
593 593 class HasRepoPermissionAnyDecorator(PermsDecorator):
594 594 """
595 595 Checks for access permission for any of given predicates for specific
596 596 repository. In order to fulfill the request any of predicates must be meet
597 597 """
598 598
599 599 def check_permissions(self):
600 600 repo_name = get_repo_slug(request)
601 601
602 602 try:
603 603 user_perms = set([self.user_perms['repositories'][repo_name]])
604 604 except KeyError:
605 605 return False
606 606
607 607 if self.required_perms.intersection(user_perms):
608 608 return True
609 609 return False
610 610
611 611
612 612 class HasReposGroupPermissionAllDecorator(PermsDecorator):
613 613 """
614 614 Checks for access permission for all given predicates for specific
615 615 repository. All of them have to be meet in order to fulfill the request
616 616 """
617 617
618 618 def check_permissions(self):
619 619 group_name = get_repos_group_slug(request)
620 620 try:
621 621 user_perms = set([self.user_perms['repositories_groups'][group_name]])
622 622 except KeyError:
623 623 return False
624 624 if self.required_perms.issubset(user_perms):
625 625 return True
626 626 return False
627 627
628 628
629 629 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
630 630 """
631 631 Checks for access permission for any of given predicates for specific
632 632 repository. In order to fulfill the request any of predicates must be meet
633 633 """
634 634
635 635 def check_permissions(self):
636 636 group_name = get_repos_group_slug(request)
637 637
638 638 try:
639 639 user_perms = set([self.user_perms['repositories_groups'][group_name]])
640 640 except KeyError:
641 641 return False
642 642 if self.required_perms.intersection(user_perms):
643 643 return True
644 644 return False
645 645
646 646
647 647 #==============================================================================
648 648 # CHECK FUNCTIONS
649 649 #==============================================================================
650 650 class PermsFunction(object):
651 651 """Base function for other check functions"""
652 652
653 653 def __init__(self, *perms):
654 654 available_perms = config['available_permissions']
655 655
656 656 for perm in perms:
657 657 if perm not in available_perms:
658 658 raise Exception("'%s' permission is not defined" % perm)
659 659 self.required_perms = set(perms)
660 660 self.user_perms = None
661 661 self.repo_name = None
662 662 self.group_name = None
663 663
664 664 def __call__(self, check_Location=''):
665 665 user = request.user
666 666 cls_name = self.__class__.__name__
667 667 check_scope = {
668 668 'HasPermissionAll': '',
669 669 'HasPermissionAny': '',
670 670 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
671 671 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
672 672 'HasReposGroupPermissionAll': 'group:%s' % self.group_name,
673 673 'HasReposGroupPermissionAny': 'group:%s' % self.group_name,
674 674 }.get(cls_name, '?')
675 675 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
676 676 self.required_perms, user, check_scope,
677 677 check_Location or 'unspecified location')
678 678 if not user:
679 679 log.debug('Empty request user')
680 680 return False
681 681 self.user_perms = user.permissions
682 682 if self.check_permissions():
683 683 log.debug('Permission granted for user: %s @ %s', user,
684 684 check_Location or 'unspecified location')
685 685 return True
686 686
687 687 else:
688 688 log.debug('Permission denied for user: %s @ %s', user,
689 689 check_Location or 'unspecified location')
690 690 return False
691 691
692 692 def check_permissions(self):
693 693 """Dummy function for overriding"""
694 694 raise Exception('You have to write this function in child class')
695 695
696 696
697 697 class HasPermissionAll(PermsFunction):
698 698 def check_permissions(self):
699 699 if self.required_perms.issubset(self.user_perms.get('global')):
700 700 return True
701 701 return False
702 702
703 703
704 704 class HasPermissionAny(PermsFunction):
705 705 def check_permissions(self):
706 706 if self.required_perms.intersection(self.user_perms.get('global')):
707 707 return True
708 708 return False
709 709
710 710
711 711 class HasRepoPermissionAll(PermsFunction):
712 712 def __call__(self, repo_name=None, check_Location=''):
713 713 self.repo_name = repo_name
714 714 return super(HasRepoPermissionAll, self).__call__(check_Location)
715 715
716 716 def check_permissions(self):
717 717 if not self.repo_name:
718 718 self.repo_name = get_repo_slug(request)
719 719
720 720 try:
721 721 self._user_perms = set(
722 722 [self.user_perms['repositories'][self.repo_name]]
723 723 )
724 724 except KeyError:
725 725 return False
726 726 if self.required_perms.issubset(self._user_perms):
727 727 return True
728 728 return False
729 729
730 730
731 731 class HasRepoPermissionAny(PermsFunction):
732 732 def __call__(self, repo_name=None, check_Location=''):
733 733 self.repo_name = repo_name
734 734 return super(HasRepoPermissionAny, self).__call__(check_Location)
735 735
736 736 def check_permissions(self):
737 737 if not self.repo_name:
738 738 self.repo_name = get_repo_slug(request)
739 739
740 740 try:
741 741 self._user_perms = set(
742 742 [self.user_perms['repositories'][self.repo_name]]
743 743 )
744 744 except KeyError:
745 745 return False
746 746 if self.required_perms.intersection(self._user_perms):
747 747 return True
748 748 return False
749 749
750 750
751 751 class HasReposGroupPermissionAny(PermsFunction):
752 752 def __call__(self, group_name=None, check_Location=''):
753 753 self.group_name = group_name
754 754 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
755 755
756 756 def check_permissions(self):
757 757 try:
758 758 self._user_perms = set(
759 759 [self.user_perms['repositories_groups'][self.group_name]]
760 760 )
761 761 except KeyError:
762 762 return False
763 763 if self.required_perms.intersection(self._user_perms):
764 764 return True
765 765 return False
766 766
767 767
768 768 class HasReposGroupPermissionAll(PermsFunction):
769 769 def __call__(self, group_name=None, check_Location=''):
770 770 self.group_name = group_name
771 771 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
772 772
773 773 def check_permissions(self):
774 774 try:
775 775 self._user_perms = set(
776 776 [self.user_perms['repositories_groups'][self.group_name]]
777 777 )
778 778 except KeyError:
779 779 return False
780 780 if self.required_perms.issubset(self._user_perms):
781 781 return True
782 782 return False
783 783
784 784
785 785 #==============================================================================
786 786 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
787 787 #==============================================================================
788 788 class HasPermissionAnyMiddleware(object):
789 789 def __init__(self, *perms):
790 790 self.required_perms = set(perms)
791 791
792 792 def __call__(self, user, repo_name):
793 793 # repo_name MUST be unicode, since we handle keys in permission
794 794 # dict by unicode
795 795 repo_name = safe_unicode(repo_name)
796 796 usr = AuthUser(user.user_id)
797 797 try:
798 798 self.user_perms = set([usr.permissions['repositories'][repo_name]])
799 799 except Exception:
800 800 log.error('Exception while accessing permissions %s' %
801 801 traceback.format_exc())
802 802 self.user_perms = set()
803 803 self.username = user.username
804 804 self.repo_name = repo_name
805 805 return self.check_permissions()
806 806
807 807 def check_permissions(self):
808 808 log.debug('checking mercurial protocol '
809 809 'permissions %s for user:%s repository:%s', self.user_perms,
810 810 self.username, self.repo_name)
811 811 if self.required_perms.intersection(self.user_perms):
812 812 log.debug('permission granted for user:%s on repo:%s' % (
813 813 self.username, self.repo_name
814 814 )
815 815 )
816 816 return True
817 817 log.debug('permission denied for user:%s on repo:%s' % (
818 818 self.username, self.repo_name
819 819 )
820 820 )
821 821 return False
@@ -1,104 +1,106 b''
1 1 import datetime
2 2 import functools
3 3 import decimal
4 4
5 5 __all__ = ['json', 'simplejson', 'stdjson']
6 6
7 7
8 8 def _is_aware(value):
9 9 """
10 10 Determines if a given datetime.time is aware.
11 11
12 12 The logic is described in Python's docs:
13 13 http://docs.python.org/library/datetime.html#datetime.tzinfo
14 14 """
15 15 return (value.tzinfo is not None
16 16 and value.tzinfo.utcoffset(value) is not None)
17 17
18 18
19 19 def _obj_dump(obj):
20 20 """
21 21 Custom function for dumping objects to JSON, if obj has __json__ attribute
22 22 or method defined it will be used for serialization
23 23
24 24 :param obj:
25 25 """
26 26
27 27 if isinstance(obj, complex):
28 28 return [obj.real, obj.imag]
29 29 # See "Date Time String Format" in the ECMA-262 specification.
30 30 # some code borrowed from django 1.4
31 31 elif isinstance(obj, datetime.datetime):
32 32 r = obj.isoformat()
33 33 if obj.microsecond:
34 34 r = r[:23] + r[26:]
35 35 if r.endswith('+00:00'):
36 36 r = r[:-6] + 'Z'
37 37 return r
38 38 elif isinstance(obj, datetime.date):
39 39 return obj.isoformat()
40 40 elif isinstance(obj, decimal.Decimal):
41 41 return str(obj)
42 42 elif isinstance(obj, datetime.time):
43 43 if _is_aware(obj):
44 44 raise ValueError("JSON can't represent timezone-aware times.")
45 45 r = obj.isoformat()
46 46 if obj.microsecond:
47 47 r = r[:12]
48 48 return r
49 49 elif isinstance(obj, set):
50 50 return list(obj)
51 51 elif hasattr(obj, '__json__'):
52 52 if callable(obj.__json__):
53 53 return obj.__json__()
54 54 else:
55 55 return obj.__json__
56 56 else:
57 57 raise NotImplementedError
58 58
59 59
60 60 # Import simplejson
61 61 try:
62 62 # import simplejson initially
63 63 import simplejson as _sj
64 64
65 65 def extended_encode(obj):
66 66 try:
67 67 return _obj_dump(obj)
68 68 except NotImplementedError:
69 69 pass
70 70 raise TypeError("%r is not JSON serializable" % (obj,))
71 71 # we handle decimals our own it makes unified behavior of json vs
72 72 # simplejson
73 73 _sj.dumps = functools.partial(_sj.dumps, default=extended_encode,
74 74 use_decimal=False)
75 75 _sj.dump = functools.partial(_sj.dump, default=extended_encode,
76 76 use_decimal=False)
77 77 simplejson = _sj
78 78
79 79 except ImportError:
80 80 # no simplejson set it to None
81 81 _sj = None
82 82
83 83
84 # simplejson not found try out regular json module
85 import json as _json
86
84 try:
85 # simplejson not found try out regular json module
86 import json as _json
87 87
88 # extended JSON encoder for json
89 class ExtendedEncoder(_json.JSONEncoder):
90 def default(self, obj):
91 try:
92 return _obj_dump(obj)
93 except NotImplementedError:
94 pass
95 return _json.JSONEncoder.default(self, obj)
96 # monkey-patch JSON encoder to use extended version
97 _json.dumps = functools.partial(_json.dumps, cls=ExtendedEncoder)
98 _json.dump = functools.partial(_json.dump, cls=ExtendedEncoder)
99 stdlib = _json
88 # extended JSON encoder for json
89 class ExtendedEncoder(_json.JSONEncoder):
90 def default(self, obj):
91 try:
92 return _obj_dump(obj)
93 except NotImplementedError:
94 pass
95 return _json.JSONEncoder.default(self, obj)
96 # monkey-patch JSON encoder to use extended version
97 _json.dumps = functools.partial(_json.dumps, cls=ExtendedEncoder)
98 _json.dump = functools.partial(_json.dump, cls=ExtendedEncoder)
99 stdlib = _json
100 except ImportError:
101 _json = None
100 102
101 103 # set all available json modules
102 104 simplejson = _sj
103 105 stdjson = _json
104 106 json = _sj if _sj else _json
@@ -1,666 +1,666 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 from os.path import abspath
36 36 from os.path import dirname as dn, join as jn
37 37
38 38 from paste.script.command import Command, BadCommand
39 39
40 40 from mercurial import ui, config
41 41
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43
44 44 from rhodecode.lib.vcs import get_backend
45 45 from rhodecode.lib.vcs.backends.base import BaseChangeset
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.utils.helpers import get_scm
48 48 from rhodecode.lib.vcs.exceptions import VCSError
49 49
50 50 from rhodecode.lib.caching_query import FromCache
51 51
52 52 from rhodecode.model import meta
53 53 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
54 54 UserLog, RepoGroup, RhodeCodeSetting, UserRepoGroupToPerm,\
55 55 CacheInvalidation
56 56 from rhodecode.model.meta import Session
57 57 from rhodecode.model.repos_group import ReposGroupModel
58 58 from rhodecode.lib.utils2 import safe_str, safe_unicode
59 59 from rhodecode.lib.vcs.utils.fakemod import create_module
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
64 64
65 65
66 66 def recursive_replace(str_, replace=' '):
67 67 """
68 68 Recursive replace of given sign to just one instance
69 69
70 70 :param str_: given string
71 71 :param replace: char to find and replace multiple instances
72 72
73 73 Examples::
74 74 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
75 75 'Mighty-Mighty-Bo-sstones'
76 76 """
77 77
78 78 if str_.find(replace * 2) == -1:
79 79 return str_
80 80 else:
81 81 str_ = str_.replace(replace * 2, replace)
82 82 return recursive_replace(str_, replace)
83 83
84 84
85 85 def repo_name_slug(value):
86 86 """
87 87 Return slug of name of repository
88 88 This function is called on each creation/modification
89 89 of repository to prevent bad names in repo
90 90 """
91 91
92 92 slug = remove_formatting(value)
93 93 slug = strip_tags(slug)
94 94
95 95 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
96 96 slug = slug.replace(c, '-')
97 97 slug = recursive_replace(slug, '-')
98 98 slug = collapse(slug, '-')
99 99 return slug
100 100
101 101
102 102 def get_repo_slug(request):
103 103 _repo = request.environ['pylons.routes_dict'].get('repo_name')
104 104 if _repo:
105 105 _repo = _repo.rstrip('/')
106 106 return _repo
107 107
108 108
109 109 def get_repos_group_slug(request):
110 110 _group = request.environ['pylons.routes_dict'].get('group_name')
111 111 if _group:
112 112 _group = _group.rstrip('/')
113 113 return _group
114 114
115 115
116 116 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
117 117 """
118 118 Action logger for various actions made by users
119 119
120 120 :param user: user that made this action, can be a unique username string or
121 121 object containing user_id attribute
122 122 :param action: action to log, should be on of predefined unique actions for
123 123 easy translations
124 124 :param repo: string name of repository or object containing repo_id,
125 125 that action was made on
126 126 :param ipaddr: optional ip address from what the action was made
127 127 :param sa: optional sqlalchemy session
128 128
129 129 """
130 130
131 131 if not sa:
132 132 sa = meta.Session
133 133
134 134 try:
135 135 if hasattr(user, 'user_id'):
136 136 user_obj = user
137 137 elif isinstance(user, basestring):
138 138 user_obj = User.get_by_username(user)
139 139 else:
140 140 raise Exception('You have to provide user object or username')
141 141
142 142 if hasattr(repo, 'repo_id'):
143 143 repo_obj = Repository.get(repo.repo_id)
144 144 repo_name = repo_obj.repo_name
145 145 elif isinstance(repo, basestring):
146 146 repo_name = repo.lstrip('/')
147 147 repo_obj = Repository.get_by_repo_name(repo_name)
148 148 else:
149 149 raise Exception('You have to provide repository to action logger')
150 150
151 151 user_log = UserLog()
152 152 user_log.user_id = user_obj.user_id
153 user_log.action = action
153 user_log.action = safe_unicode(action)
154 154
155 155 user_log.repository_id = repo_obj.repo_id
156 156 user_log.repository_name = repo_name
157 157
158 158 user_log.action_date = datetime.datetime.now()
159 159 user_log.user_ip = ipaddr
160 160 sa.add(user_log)
161 161
162 162 log.info(
163 163 'Adding user %s, action %s on %s' % (user_obj, action,
164 164 safe_unicode(repo))
165 165 )
166 166 if commit:
167 167 sa.commit()
168 168 except:
169 169 log.error(traceback.format_exc())
170 170 raise
171 171
172 172
173 173 def get_repos(path, recursive=False):
174 174 """
175 175 Scans given path for repos and return (name,(type,path)) tuple
176 176
177 177 :param path: path to scan for repositories
178 178 :param recursive: recursive search and return names with subdirs in front
179 179 """
180 180
181 181 # remove ending slash for better results
182 182 path = path.rstrip(os.sep)
183 183
184 184 def _get_repos(p):
185 185 if not os.access(p, os.W_OK):
186 186 return
187 187 for dirpath in os.listdir(p):
188 188 if os.path.isfile(os.path.join(p, dirpath)):
189 189 continue
190 190 cur_path = os.path.join(p, dirpath)
191 191 try:
192 192 scm_info = get_scm(cur_path)
193 193 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
194 194 except VCSError:
195 195 if not recursive:
196 196 continue
197 197 #check if this dir containts other repos for recursive scan
198 198 rec_path = os.path.join(p, dirpath)
199 199 if os.path.isdir(rec_path):
200 200 for inner_scm in _get_repos(rec_path):
201 201 yield inner_scm
202 202
203 203 return _get_repos(path)
204 204
205 205
206 206 def is_valid_repo(repo_name, base_path):
207 207 """
208 208 Returns True if given path is a valid repository False otherwise
209 209
210 210 :param repo_name:
211 211 :param base_path:
212 212
213 213 :return True: if given path is a valid repository
214 214 """
215 215 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
216 216
217 217 try:
218 218 get_scm(full_path)
219 219 return True
220 220 except VCSError:
221 221 return False
222 222
223 223
224 224 def is_valid_repos_group(repos_group_name, base_path):
225 225 """
226 226 Returns True if given path is a repos group False otherwise
227 227
228 228 :param repo_name:
229 229 :param base_path:
230 230 """
231 231 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
232 232
233 233 # check if it's not a repo
234 234 if is_valid_repo(repos_group_name, base_path):
235 235 return False
236 236
237 237 # check if it's a valid path
238 238 if os.path.isdir(full_path):
239 239 return True
240 240
241 241 return False
242 242
243 243
244 244 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
245 245 while True:
246 246 ok = raw_input(prompt)
247 247 if ok in ('y', 'ye', 'yes'):
248 248 return True
249 249 if ok in ('n', 'no', 'nop', 'nope'):
250 250 return False
251 251 retries = retries - 1
252 252 if retries < 0:
253 253 raise IOError
254 254 print complaint
255 255
256 256 #propagated from mercurial documentation
257 257 ui_sections = ['alias', 'auth',
258 258 'decode/encode', 'defaults',
259 259 'diff', 'email',
260 260 'extensions', 'format',
261 261 'merge-patterns', 'merge-tools',
262 262 'hooks', 'http_proxy',
263 263 'smtp', 'patch',
264 264 'paths', 'profiling',
265 265 'server', 'trusted',
266 266 'ui', 'web', ]
267 267
268 268
269 269 def make_ui(read_from='file', path=None, checkpaths=True):
270 270 """
271 271 A function that will read python rc files or database
272 272 and make an mercurial ui object from read options
273 273
274 274 :param path: path to mercurial config file
275 275 :param checkpaths: check the path
276 276 :param read_from: read from 'file' or 'db'
277 277 """
278 278
279 279 baseui = ui.ui()
280 280
281 281 # clean the baseui object
282 282 baseui._ocfg = config.config()
283 283 baseui._ucfg = config.config()
284 284 baseui._tcfg = config.config()
285 285
286 286 if read_from == 'file':
287 287 if not os.path.isfile(path):
288 288 log.debug('hgrc file is not present at %s skipping...' % path)
289 289 return False
290 290 log.debug('reading hgrc from %s' % path)
291 291 cfg = config.config()
292 292 cfg.read(path)
293 293 for section in ui_sections:
294 294 for k, v in cfg.items(section):
295 295 log.debug('settings ui from file[%s]%s:%s' % (section, k, v))
296 296 baseui.setconfig(section, k, v)
297 297
298 298 elif read_from == 'db':
299 299 sa = meta.Session
300 300 ret = sa.query(RhodeCodeUi)\
301 301 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
302 302 .all()
303 303
304 304 hg_ui = ret
305 305 for ui_ in hg_ui:
306 306 if ui_.ui_active:
307 307 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
308 308 ui_.ui_key, ui_.ui_value)
309 309 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
310 310
311 311 meta.Session.remove()
312 312 return baseui
313 313
314 314
315 315 def set_rhodecode_config(config):
316 316 """
317 317 Updates pylons config with new settings from database
318 318
319 319 :param config:
320 320 """
321 321 hgsettings = RhodeCodeSetting.get_app_settings()
322 322
323 323 for k, v in hgsettings.items():
324 324 config[k] = v
325 325
326 326
327 327 def invalidate_cache(cache_key, *args):
328 328 """
329 329 Puts cache invalidation task into db for
330 330 further global cache invalidation
331 331 """
332 332
333 333 from rhodecode.model.scm import ScmModel
334 334
335 335 if cache_key.startswith('get_repo_cached_'):
336 336 name = cache_key.split('get_repo_cached_')[-1]
337 337 ScmModel().mark_for_invalidation(name)
338 338
339 339
340 340 class EmptyChangeset(BaseChangeset):
341 341 """
342 342 An dummy empty changeset. It's possible to pass hash when creating
343 343 an EmptyChangeset
344 344 """
345 345
346 346 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
347 347 alias=None):
348 348 self._empty_cs = cs
349 349 self.revision = -1
350 350 self.message = ''
351 351 self.author = ''
352 352 self.date = ''
353 353 self.repository = repo
354 354 self.requested_revision = requested_revision
355 355 self.alias = alias
356 356
357 357 @LazyProperty
358 358 def raw_id(self):
359 359 """
360 360 Returns raw string identifying this changeset, useful for web
361 361 representation.
362 362 """
363 363
364 364 return self._empty_cs
365 365
366 366 @LazyProperty
367 367 def branch(self):
368 368 return get_backend(self.alias).DEFAULT_BRANCH_NAME
369 369
370 370 @LazyProperty
371 371 def short_id(self):
372 372 return self.raw_id[:12]
373 373
374 374 def get_file_changeset(self, path):
375 375 return self
376 376
377 377 def get_file_content(self, path):
378 378 return u''
379 379
380 380 def get_file_size(self, path):
381 381 return 0
382 382
383 383
384 384 def map_groups(path):
385 385 """
386 386 Given a full path to a repository, create all nested groups that this
387 387 repo is inside. This function creates parent-child relationships between
388 388 groups and creates default perms for all new groups.
389 389
390 390 :param paths: full path to repository
391 391 """
392 392 sa = meta.Session
393 393 groups = path.split(Repository.url_sep())
394 394 parent = None
395 395 group = None
396 396
397 397 # last element is repo in nested groups structure
398 398 groups = groups[:-1]
399 399 rgm = ReposGroupModel(sa)
400 400 for lvl, group_name in enumerate(groups):
401 401 group_name = '/'.join(groups[:lvl] + [group_name])
402 402 group = RepoGroup.get_by_group_name(group_name)
403 403 desc = '%s group' % group_name
404 404
405 405 # skip folders that are now removed repos
406 406 if REMOVED_REPO_PAT.match(group_name):
407 407 break
408 408
409 409 if group is None:
410 410 log.debug('creating group level: %s group_name: %s' % (lvl,
411 411 group_name))
412 412 group = RepoGroup(group_name, parent)
413 413 group.group_description = desc
414 414 sa.add(group)
415 415 rgm._create_default_perms(group)
416 416 sa.flush()
417 417 parent = group
418 418 return group
419 419
420 420
421 421 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
422 422 """
423 423 maps all repos given in initial_repo_list, non existing repositories
424 424 are created, if remove_obsolete is True it also check for db entries
425 425 that are not in initial_repo_list and removes them.
426 426
427 427 :param initial_repo_list: list of repositories found by scanning methods
428 428 :param remove_obsolete: check for obsolete entries in database
429 429 """
430 430 from rhodecode.model.repo import RepoModel
431 431 sa = meta.Session
432 432 rm = RepoModel()
433 433 user = sa.query(User).filter(User.admin == True).first()
434 434 if user is None:
435 435 raise Exception('Missing administrative account !')
436 436 added = []
437 437
438 438 for name, repo in initial_repo_list.items():
439 439 group = map_groups(name)
440 440 if not rm.get_by_repo_name(name, cache=False):
441 441 log.info('repository %s not found creating default' % name)
442 442 added.append(name)
443 443 form_data = {
444 444 'repo_name': name,
445 445 'repo_name_full': name,
446 446 'repo_type': repo.alias,
447 447 'description': repo.description \
448 448 if repo.description != 'unknown' else '%s repository' % name,
449 449 'private': False,
450 450 'group_id': getattr(group, 'group_id', None)
451 451 }
452 452 rm.create(form_data, user, just_db=True)
453 453 sa.commit()
454 454 removed = []
455 455 if remove_obsolete:
456 456 # remove from database those repositories that are not in the filesystem
457 457 for repo in sa.query(Repository).all():
458 458 if repo.repo_name not in initial_repo_list.keys():
459 459 log.debug("Removing non existing repository found in db %s" %
460 460 repo.repo_name)
461 461 removed.append(repo.repo_name)
462 462 sa.delete(repo)
463 463 sa.commit()
464 464
465 465 # clear cache keys
466 466 log.debug("Clearing cache keys now...")
467 467 CacheInvalidation.clear_cache()
468 468 sa.commit()
469 469 return added, removed
470 470
471 471
472 472 # set cache regions for beaker so celery can utilise it
473 473 def add_cache(settings):
474 474 cache_settings = {'regions': None}
475 475 for key in settings.keys():
476 476 for prefix in ['beaker.cache.', 'cache.']:
477 477 if key.startswith(prefix):
478 478 name = key.split(prefix)[1].strip()
479 479 cache_settings[name] = settings[key].strip()
480 480 if cache_settings['regions']:
481 481 for region in cache_settings['regions'].split(','):
482 482 region = region.strip()
483 483 region_settings = {}
484 484 for key, value in cache_settings.items():
485 485 if key.startswith(region):
486 486 region_settings[key.split('.')[1]] = value
487 487 region_settings['expire'] = int(region_settings.get('expire',
488 488 60))
489 489 region_settings.setdefault('lock_dir',
490 490 cache_settings.get('lock_dir'))
491 491 region_settings.setdefault('data_dir',
492 492 cache_settings.get('data_dir'))
493 493
494 494 if 'type' not in region_settings:
495 495 region_settings['type'] = cache_settings.get('type',
496 496 'memory')
497 497 beaker.cache.cache_regions[region] = region_settings
498 498
499 499
500 500 def load_rcextensions(root_path):
501 501 import rhodecode
502 502 from rhodecode.config import conf
503 503
504 504 path = os.path.join(root_path, 'rcextensions', '__init__.py')
505 505 if os.path.isfile(path):
506 506 rcext = create_module('rc', path)
507 507 EXT = rhodecode.EXTENSIONS = rcext
508 508 log.debug('Found rcextensions now loading %s...' % rcext)
509 509
510 510 # Additional mappings that are not present in the pygments lexers
511 511 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
512 512
513 513 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
514 514
515 515 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
516 516 log.debug('settings custom INDEX_EXTENSIONS')
517 517 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
518 518
519 519 #ADDITIONAL MAPPINGS
520 520 log.debug('adding extra into INDEX_EXTENSIONS')
521 521 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
522 522
523 523
524 524 #==============================================================================
525 525 # TEST FUNCTIONS AND CREATORS
526 526 #==============================================================================
527 527 def create_test_index(repo_location, config, full_index):
528 528 """
529 529 Makes default test index
530 530
531 531 :param config: test config
532 532 :param full_index:
533 533 """
534 534
535 535 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
536 536 from rhodecode.lib.pidlock import DaemonLock, LockHeld
537 537
538 538 repo_location = repo_location
539 539
540 540 index_location = os.path.join(config['app_conf']['index_dir'])
541 541 if not os.path.exists(index_location):
542 542 os.makedirs(index_location)
543 543
544 544 try:
545 545 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
546 546 WhooshIndexingDaemon(index_location=index_location,
547 547 repo_location=repo_location)\
548 548 .run(full_index=full_index)
549 549 l.release()
550 550 except LockHeld:
551 551 pass
552 552
553 553
554 554 def create_test_env(repos_test_path, config):
555 555 """
556 556 Makes a fresh database and
557 557 install test repository into tmp dir
558 558 """
559 559 from rhodecode.lib.db_manage import DbManage
560 560 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
561 561
562 562 # PART ONE create db
563 563 dbconf = config['sqlalchemy.db1.url']
564 564 log.debug('making test db %s' % dbconf)
565 565
566 566 # create test dir if it doesn't exist
567 567 if not os.path.isdir(repos_test_path):
568 568 log.debug('Creating testdir %s' % repos_test_path)
569 569 os.makedirs(repos_test_path)
570 570
571 571 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
572 572 tests=True)
573 573 dbmanage.create_tables(override=True)
574 574 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
575 575 dbmanage.create_default_user()
576 576 dbmanage.admin_prompt()
577 577 dbmanage.create_permissions()
578 578 dbmanage.populate_default_permissions()
579 579 Session.commit()
580 580 # PART TWO make test repo
581 581 log.debug('making test vcs repositories')
582 582
583 583 idx_path = config['app_conf']['index_dir']
584 584 data_path = config['app_conf']['cache_dir']
585 585
586 586 #clean index and data
587 587 if idx_path and os.path.exists(idx_path):
588 588 log.debug('remove %s' % idx_path)
589 589 shutil.rmtree(idx_path)
590 590
591 591 if data_path and os.path.exists(data_path):
592 592 log.debug('remove %s' % data_path)
593 593 shutil.rmtree(data_path)
594 594
595 595 #CREATE DEFAULT HG REPOSITORY
596 596 cur_dir = dn(dn(abspath(__file__)))
597 597 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
598 598 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
599 599 tar.close()
600 600
601 601
602 602 #==============================================================================
603 603 # PASTER COMMANDS
604 604 #==============================================================================
605 605 class BasePasterCommand(Command):
606 606 """
607 607 Abstract Base Class for paster commands.
608 608
609 609 The celery commands are somewhat aggressive about loading
610 610 celery.conf, and since our module sets the `CELERY_LOADER`
611 611 environment variable to our loader, we have to bootstrap a bit and
612 612 make sure we've had a chance to load the pylons config off of the
613 613 command line, otherwise everything fails.
614 614 """
615 615 min_args = 1
616 616 min_args_error = "Please provide a paster config file as an argument."
617 617 takes_config_file = 1
618 618 requires_config_file = True
619 619
620 620 def notify_msg(self, msg, log=False):
621 621 """Make a notification to user, additionally if logger is passed
622 622 it logs this action using given logger
623 623
624 624 :param msg: message that will be printed to user
625 625 :param log: logging instance, to use to additionally log this message
626 626
627 627 """
628 628 if log and isinstance(log, logging):
629 629 log(msg)
630 630
631 631 def run(self, args):
632 632 """
633 633 Overrides Command.run
634 634
635 635 Checks for a config file argument and loads it.
636 636 """
637 637 if len(args) < self.min_args:
638 638 raise BadCommand(
639 639 self.min_args_error % {'min_args': self.min_args,
640 640 'actual_args': len(args)})
641 641
642 642 # Decrement because we're going to lob off the first argument.
643 643 # @@ This is hacky
644 644 self.min_args -= 1
645 645 self.bootstrap_config(args[0])
646 646 self.update_parser()
647 647 return super(BasePasterCommand, self).run(args[1:])
648 648
649 649 def update_parser(self):
650 650 """
651 651 Abstract method. Allows for the class's parser to be updated
652 652 before the superclass's `run` method is called. Necessary to
653 653 allow options/arguments to be passed through to the underlying
654 654 celery command.
655 655 """
656 656 raise NotImplementedError("Abstract Method.")
657 657
658 658 def bootstrap_config(self, conf):
659 659 """
660 660 Loads the pylons configuration.
661 661 """
662 662 from pylons import config as pylonsconfig
663 663
664 664 self.path_to_ini_file = os.path.realpath(conf)
665 665 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
666 666 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,408 +1,407 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import re
27 27 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28 28
29 29
30 30 def __get_lem():
31 31 """
32 32 Get language extension map based on what's inside pygments lexers
33 33 """
34 34 from pygments import lexers
35 35 from string import lower
36 36 from collections import defaultdict
37 37
38 38 d = defaultdict(lambda: [])
39 39
40 40 def __clean(s):
41 41 s = s.lstrip('*')
42 42 s = s.lstrip('.')
43 43
44 44 if s.find('[') != -1:
45 45 exts = []
46 46 start, stop = s.find('['), s.find(']')
47 47
48 48 for suffix in s[start + 1:stop]:
49 49 exts.append(s[:s.find('[')] + suffix)
50 50 return map(lower, exts)
51 51 else:
52 52 return map(lower, [s])
53 53
54 54 for lx, t in sorted(lexers.LEXERS.items()):
55 55 m = map(__clean, t[-2])
56 56 if m:
57 57 m = reduce(lambda x, y: x + y, m)
58 58 for ext in m:
59 59 desc = lx.replace('Lexer', '')
60 60 d[ext].append(desc)
61 61
62 62 return dict(d)
63 63
64 64 def str2bool(_str):
65 65 """
66 66 returs True/False value from given string, it tries to translate the
67 67 string into boolean
68 68
69 69 :param _str: string value to translate into boolean
70 70 :rtype: boolean
71 71 :returns: boolean from given string
72 72 """
73 73 if _str is None:
74 74 return False
75 75 if _str in (True, False):
76 76 return _str
77 77 _str = str(_str).strip().lower()
78 78 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
79 79
80 80
81 81 def convert_line_endings(line, mode):
82 82 """
83 83 Converts a given line "line end" accordingly to given mode
84 84
85 85 Available modes are::
86 86 0 - Unix
87 87 1 - Mac
88 88 2 - DOS
89 89
90 90 :param line: given line to convert
91 91 :param mode: mode to convert to
92 92 :rtype: str
93 93 :return: converted line according to mode
94 94 """
95 95 from string import replace
96 96
97 97 if mode == 0:
98 98 line = replace(line, '\r\n', '\n')
99 99 line = replace(line, '\r', '\n')
100 100 elif mode == 1:
101 101 line = replace(line, '\r\n', '\r')
102 102 line = replace(line, '\n', '\r')
103 103 elif mode == 2:
104 104 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
105 105 return line
106 106
107 107
108 108 def detect_mode(line, default):
109 109 """
110 110 Detects line break for given line, if line break couldn't be found
111 111 given default value is returned
112 112
113 113 :param line: str line
114 114 :param default: default
115 115 :rtype: int
116 116 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
117 117 """
118 118 if line.endswith('\r\n'):
119 119 return 2
120 120 elif line.endswith('\n'):
121 121 return 0
122 122 elif line.endswith('\r'):
123 123 return 1
124 124 else:
125 125 return default
126 126
127 127
128 128 def generate_api_key(username, salt=None):
129 129 """
130 130 Generates unique API key for given username, if salt is not given
131 131 it'll be generated from some random string
132 132
133 133 :param username: username as string
134 134 :param salt: salt to hash generate KEY
135 135 :rtype: str
136 136 :returns: sha1 hash from username+salt
137 137 """
138 138 from tempfile import _RandomNameSequence
139 139 import hashlib
140 140
141 141 if salt is None:
142 142 salt = _RandomNameSequence().next()
143 143
144 144 return hashlib.sha1(username + salt).hexdigest()
145 145
146 146
147 147 def safe_unicode(str_, from_encoding=None):
148 148 """
149 149 safe unicode function. Does few trick to turn str_ into unicode
150 150
151 151 In case of UnicodeDecode error we try to return it with encoding detected
152 152 by chardet library if it fails fallback to unicode with errors replaced
153 153
154 154 :param str_: string to decode
155 155 :rtype: unicode
156 156 :returns: unicode object
157 157 """
158 158 if isinstance(str_, unicode):
159 159 return str_
160 160
161 161 if not from_encoding:
162 162 import rhodecode
163 163 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
164 164 from_encoding = DEFAULT_ENCODING
165 165
166 166 try:
167 167 return unicode(str_)
168 168 except UnicodeDecodeError:
169 169 pass
170 170
171 171 try:
172 172 return unicode(str_, from_encoding)
173 173 except UnicodeDecodeError:
174 174 pass
175 175
176 176 try:
177 177 import chardet
178 178 encoding = chardet.detect(str_)['encoding']
179 179 if encoding is None:
180 180 raise Exception()
181 181 return str_.decode(encoding)
182 182 except (ImportError, UnicodeDecodeError, Exception):
183 183 return unicode(str_, from_encoding, 'replace')
184 184
185 185
186 186 def safe_str(unicode_, to_encoding=None):
187 187 """
188 188 safe str function. Does few trick to turn unicode_ into string
189 189
190 190 In case of UnicodeEncodeError we try to return it with encoding detected
191 191 by chardet library if it fails fallback to string with errors replaced
192 192
193 193 :param unicode_: unicode to encode
194 194 :rtype: str
195 195 :returns: str object
196 196 """
197 197
198 198 # if it's not basestr cast to str
199 199 if not isinstance(unicode_, basestring):
200 200 return str(unicode_)
201 201
202 202 if isinstance(unicode_, str):
203 203 return unicode_
204 204
205 205 if not to_encoding:
206 206 import rhodecode
207 207 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
208 208 to_encoding = DEFAULT_ENCODING
209 209
210 210 try:
211 211 return unicode_.encode(to_encoding)
212 212 except UnicodeEncodeError:
213 213 pass
214 214
215 215 try:
216 216 import chardet
217 217 encoding = chardet.detect(unicode_)['encoding']
218 print encoding
219 218 if encoding is None:
220 219 raise UnicodeEncodeError()
221 220
222 221 return unicode_.encode(encoding)
223 222 except (ImportError, UnicodeEncodeError):
224 223 return unicode_.encode(to_encoding, 'replace')
225 224
226 225 return safe_str
227 226
228 227
229 228 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
230 229 """
231 230 Custom engine_from_config functions that makes sure we use NullPool for
232 231 file based sqlite databases. This prevents errors on sqlite. This only
233 232 applies to sqlalchemy versions < 0.7.0
234 233
235 234 """
236 235 import sqlalchemy
237 236 from sqlalchemy import engine_from_config as efc
238 237 import logging
239 238
240 239 if int(sqlalchemy.__version__.split('.')[1]) < 7:
241 240
242 241 # This solution should work for sqlalchemy < 0.7.0, and should use
243 242 # proxy=TimerProxy() for execution time profiling
244 243
245 244 from sqlalchemy.pool import NullPool
246 245 url = configuration[prefix + 'url']
247 246
248 247 if url.startswith('sqlite'):
249 248 kwargs.update({'poolclass': NullPool})
250 249 return efc(configuration, prefix, **kwargs)
251 250 else:
252 251 import time
253 252 from sqlalchemy import event
254 253 from sqlalchemy.engine import Engine
255 254
256 255 log = logging.getLogger('sqlalchemy.engine')
257 256 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
258 257 engine = efc(configuration, prefix, **kwargs)
259 258
260 259 def color_sql(sql):
261 260 COLOR_SEQ = "\033[1;%dm"
262 261 COLOR_SQL = YELLOW
263 262 normal = '\x1b[0m'
264 263 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
265 264
266 265 if configuration['debug']:
267 266 #attach events only for debug configuration
268 267
269 268 def before_cursor_execute(conn, cursor, statement,
270 269 parameters, context, executemany):
271 270 context._query_start_time = time.time()
272 271 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
273 272
274 273
275 274 def after_cursor_execute(conn, cursor, statement,
276 275 parameters, context, executemany):
277 276 total = time.time() - context._query_start_time
278 277 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
279 278
280 279 event.listen(engine, "before_cursor_execute",
281 280 before_cursor_execute)
282 281 event.listen(engine, "after_cursor_execute",
283 282 after_cursor_execute)
284 283
285 284 return engine
286 285
287 286
288 287 def age(curdate):
289 288 """
290 289 turns a datetime into an age string.
291 290
292 291 :param curdate: datetime object
293 292 :rtype: unicode
294 293 :returns: unicode words describing age
295 294 """
296 295
297 296 from datetime import datetime
298 297 from webhelpers.date import time_ago_in_words
299 298
300 299 _ = lambda s: s
301 300
302 301 if not curdate:
303 302 return ''
304 303
305 304 agescales = [(_(u"year"), 3600 * 24 * 365),
306 305 (_(u"month"), 3600 * 24 * 30),
307 306 (_(u"day"), 3600 * 24),
308 307 (_(u"hour"), 3600),
309 308 (_(u"minute"), 60),
310 309 (_(u"second"), 1), ]
311 310
312 311 age = datetime.now() - curdate
313 312 age_seconds = (age.days * agescales[2][1]) + age.seconds
314 313 pos = 1
315 314 for scale in agescales:
316 315 if scale[1] <= age_seconds:
317 316 if pos == 6:
318 317 pos = 5
319 318 return '%s %s' % (time_ago_in_words(curdate,
320 319 agescales[pos][0]), _('ago'))
321 320 pos += 1
322 321
323 322 return _(u'just now')
324 323
325 324
326 325 def uri_filter(uri):
327 326 """
328 327 Removes user:password from given url string
329 328
330 329 :param uri:
331 330 :rtype: unicode
332 331 :returns: filtered list of strings
333 332 """
334 333 if not uri:
335 334 return ''
336 335
337 336 proto = ''
338 337
339 338 for pat in ('https://', 'http://'):
340 339 if uri.startswith(pat):
341 340 uri = uri[len(pat):]
342 341 proto = pat
343 342 break
344 343
345 344 # remove passwords and username
346 345 uri = uri[uri.find('@') + 1:]
347 346
348 347 # get the port
349 348 cred_pos = uri.find(':')
350 349 if cred_pos == -1:
351 350 host, port = uri, None
352 351 else:
353 352 host, port = uri[:cred_pos], uri[cred_pos + 1:]
354 353
355 354 return filter(None, [proto, host, port])
356 355
357 356
358 357 def credentials_filter(uri):
359 358 """
360 359 Returns a url with removed credentials
361 360
362 361 :param uri:
363 362 """
364 363
365 364 uri = uri_filter(uri)
366 365 #check if we have port
367 366 if len(uri) > 2 and uri[2]:
368 367 uri[2] = ':' + uri[2]
369 368
370 369 return ''.join(uri)
371 370
372 371
373 372 def get_changeset_safe(repo, rev):
374 373 """
375 374 Safe version of get_changeset if this changeset doesn't exists for a
376 375 repo it returns a Dummy one instead
377 376
378 377 :param repo:
379 378 :param rev:
380 379 """
381 380 from rhodecode.lib.vcs.backends.base import BaseRepository
382 381 from rhodecode.lib.vcs.exceptions import RepositoryError
383 382 if not isinstance(repo, BaseRepository):
384 383 raise Exception('You must pass an Repository '
385 384 'object as first argument got %s', type(repo))
386 385
387 386 try:
388 387 cs = repo.get_changeset(rev)
389 388 except RepositoryError:
390 389 from rhodecode.lib.utils import EmptyChangeset
391 390 cs = EmptyChangeset(requested_revision=rev)
392 391 return cs
393 392
394 393
395 394 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
396 395
397 396
398 397 def extract_mentioned_users(s):
399 398 """
400 399 Returns unique usernames from given string s that have @mention
401 400
402 401 :param s: string to get mentions
403 402 """
404 403 usrs = set()
405 404 for username in re.findall(MENTIONS_REGEX, s):
406 405 usrs.add(username)
407 406
408 407 return sorted(list(usrs), key=lambda k: k.lower())
@@ -1,460 +1,460 b''
1 1 import re
2 2 from itertools import chain
3 3 from dulwich import objects
4 4 from subprocess import Popen, PIPE
5 5 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 9 from rhodecode.lib.vcs.exceptions import VCSError
10 10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 14 RemovedFileNode, SubModuleNode
15 15 from rhodecode.lib.vcs.utils import safe_unicode
16 16 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 18
19 19
20 20 class GitChangeset(BaseChangeset):
21 21 """
22 22 Represents state of the repository at single revision.
23 23 """
24 24
25 25 def __init__(self, repository, revision):
26 26 self._stat_modes = {}
27 27 self.repository = repository
28 28 self.raw_id = revision
29 29 self.revision = repository.revisions.index(revision)
30 30
31 31 self.short_id = self.raw_id[:12]
32 32 self.id = self.raw_id
33 33 try:
34 34 commit = self.repository._repo.get_object(self.raw_id)
35 35 except KeyError:
36 36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
37 37 self._commit = commit
38 38 self._tree_id = commit.tree
39 39
40 40 try:
41 41 self.message = safe_unicode(commit.message[:-1])
42 42 # Always strip last eol
43 43 except UnicodeDecodeError:
44 44 self.message = commit.message[:-1].decode(commit.encoding
45 45 or 'utf-8')
46 46 #self.branch = None
47 47 self.tags = []
48 48 #tree = self.repository.get_object(self._tree_id)
49 49 self.nodes = {}
50 50 self._paths = {}
51 51
52 52 @LazyProperty
53 53 def author(self):
54 54 return safe_unicode(self._commit.committer)
55 55
56 56 @LazyProperty
57 57 def date(self):
58 58 return date_fromtimestamp(self._commit.commit_time,
59 59 self._commit.commit_timezone)
60 60
61 61 @LazyProperty
62 62 def status(self):
63 63 """
64 64 Returns modified, added, removed, deleted files for current changeset
65 65 """
66 66 return self.changed, self.added, self.removed
67 67
68 68 @LazyProperty
69 69 def branch(self):
70 70
71 71 heads = self.repository._heads(reverse=False)
72 72
73 73 ref = heads.get(self.raw_id)
74 74 if ref:
75 75 return safe_unicode(ref)
76 76
77 77 def _fix_path(self, path):
78 78 """
79 79 Paths are stored without trailing slash so we need to get rid off it if
80 80 needed.
81 81 """
82 82 if path.endswith('/'):
83 83 path = path.rstrip('/')
84 84 return path
85 85
86 86 def _get_id_for_path(self, path):
87 87
88 88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
89 89 if not path in self._paths:
90 90 path = path.strip('/')
91 91 # set root tree
92 92 tree = self.repository._repo[self._commit.tree]
93 93 if path == '':
94 94 self._paths[''] = tree.id
95 95 return tree.id
96 96 splitted = path.split('/')
97 97 dirs, name = splitted[:-1], splitted[-1]
98 98 curdir = ''
99 99
100 100 # initially extract things from root dir
101 101 for item, stat, id in tree.iteritems():
102 102 if curdir:
103 103 name = '/'.join((curdir, item))
104 104 else:
105 105 name = item
106 106 self._paths[name] = id
107 107 self._stat_modes[name] = stat
108 108
109 109 for dir in dirs:
110 110 if curdir:
111 111 curdir = '/'.join((curdir, dir))
112 112 else:
113 113 curdir = dir
114 114 dir_id = None
115 115 for item, stat, id in tree.iteritems():
116 116 if dir == item:
117 117 dir_id = id
118 118 if dir_id:
119 119 # Update tree
120 120 tree = self.repository._repo[dir_id]
121 121 if not isinstance(tree, objects.Tree):
122 122 raise ChangesetError('%s is not a directory' % curdir)
123 123 else:
124 124 raise ChangesetError('%s have not been found' % curdir)
125 125
126 126 # cache all items from the given traversed tree
127 127 for item, stat, id in tree.iteritems():
128 128 if curdir:
129 129 name = '/'.join((curdir, item))
130 130 else:
131 131 name = item
132 132 self._paths[name] = id
133 133 self._stat_modes[name] = stat
134 134 if not path in self._paths:
135 135 raise NodeDoesNotExistError("There is no file nor directory "
136 136 "at the given path %r at revision %r"
137 137 % (path, self.short_id))
138 138 return self._paths[path]
139 139
140 140 def _get_kind(self, path):
141 141 id = self._get_id_for_path(path)
142 142 obj = self.repository._repo[id]
143 143 if isinstance(obj, objects.Blob):
144 144 return NodeKind.FILE
145 145 elif isinstance(obj, objects.Tree):
146 146 return NodeKind.DIR
147 147
148 148 def _get_file_nodes(self):
149 149 return chain(*(t[2] for t in self.walk()))
150 150
151 151 @LazyProperty
152 152 def parents(self):
153 153 """
154 154 Returns list of parents changesets.
155 155 """
156 156 return [self.repository.get_changeset(parent)
157 157 for parent in self._commit.parents]
158 158
159 159 def next(self, branch=None):
160 160
161 161 if branch and self.branch != branch:
162 162 raise VCSError('Branch option used on changeset not belonging '
163 163 'to that branch')
164 164
165 165 def _next(changeset, branch):
166 166 try:
167 167 next_ = changeset.revision + 1
168 168 next_rev = changeset.repository.revisions[next_]
169 169 except IndexError:
170 170 raise ChangesetDoesNotExistError
171 171 cs = changeset.repository.get_changeset(next_rev)
172 172
173 173 if branch and branch != cs.branch:
174 174 return _next(cs, branch)
175 175
176 176 return cs
177 177
178 178 return _next(self, branch)
179 179
180 180 def prev(self, branch=None):
181 181 if branch and self.branch != branch:
182 182 raise VCSError('Branch option used on changeset not belonging '
183 183 'to that branch')
184 184
185 185 def _prev(changeset, branch):
186 186 try:
187 187 prev_ = changeset.revision - 1
188 188 if prev_ < 0:
189 189 raise IndexError
190 190 prev_rev = changeset.repository.revisions[prev_]
191 191 except IndexError:
192 192 raise ChangesetDoesNotExistError
193 193
194 194 cs = changeset.repository.get_changeset(prev_rev)
195 195
196 196 if branch and branch != cs.branch:
197 197 return _prev(cs, branch)
198 198
199 199 return cs
200 200
201 201 return _prev(self, branch)
202 202
203 203 def get_file_mode(self, path):
204 204 """
205 205 Returns stat mode of the file at the given ``path``.
206 206 """
207 207 # ensure path is traversed
208 208 self._get_id_for_path(path)
209 209 return self._stat_modes[path]
210 210
211 211 def get_file_content(self, path):
212 212 """
213 213 Returns content of the file at given ``path``.
214 214 """
215 215 id = self._get_id_for_path(path)
216 216 blob = self.repository._repo[id]
217 217 return blob.as_pretty_string()
218 218
219 219 def get_file_size(self, path):
220 220 """
221 221 Returns size of the file at given ``path``.
222 222 """
223 223 id = self._get_id_for_path(path)
224 224 blob = self.repository._repo[id]
225 225 return blob.raw_length()
226 226
227 227 def get_file_changeset(self, path):
228 228 """
229 229 Returns last commit of the file at the given ``path``.
230 230 """
231 231 node = self.get_node(path)
232 232 return node.history[0]
233 233
234 234 def get_file_history(self, path):
235 235 """
236 236 Returns history of file as reversed list of ``Changeset`` objects for
237 237 which file at given ``path`` has been modified.
238 238
239 239 TODO: This function now uses os underlying 'git' and 'grep' commands
240 240 which is generally not good. Should be replaced with algorithm
241 241 iterating commits.
242 242 """
243 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
243 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
244 244 self.id, path
245 245 )
246 246 so, se = self.repository.run_git_command(cmd)
247 ids = re.findall(r'\w{40}', so)
247 ids = re.findall(r'[0-9a-fA-F]{40}', so)
248 248 return [self.repository.get_changeset(id) for id in ids]
249 249
250 250 def get_file_annotate(self, path):
251 251 """
252 252 Returns a list of three element tuples with lineno,changeset and line
253 253
254 254 TODO: This function now uses os underlying 'git' command which is
255 255 generally not good. Should be replaced with algorithm iterating
256 256 commits.
257 257 """
258 258 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
259 259 # -l ==> outputs long shas (and we need all 40 characters)
260 260 # --root ==> doesn't put '^' character for bounderies
261 261 # -r sha ==> blames for the given revision
262 262 so, se = self.repository.run_git_command(cmd)
263 263 annotate = []
264 264 for i, blame_line in enumerate(so.split('\n')[:-1]):
265 265 ln_no = i + 1
266 266 id, line = re.split(r' \(.+?\) ', blame_line, 1)
267 267 annotate.append((ln_no, self.repository.get_changeset(id), line))
268 268 return annotate
269 269
270 270 def fill_archive(self, stream=None, kind='tgz', prefix=None,
271 271 subrepos=False):
272 272 """
273 273 Fills up given stream.
274 274
275 275 :param stream: file like object.
276 276 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
277 277 Default: ``tgz``.
278 278 :param prefix: name of root directory in archive.
279 279 Default is repository name and changeset's raw_id joined with dash
280 280 (``repo-tip.<KIND>``).
281 281 :param subrepos: include subrepos in this archive.
282 282
283 283 :raise ImproperArchiveTypeError: If given kind is wrong.
284 284 :raise VcsError: If given stream is None
285 285
286 286 """
287 287 allowed_kinds = settings.ARCHIVE_SPECS.keys()
288 288 if kind not in allowed_kinds:
289 289 raise ImproperArchiveTypeError('Archive kind not supported use one'
290 290 'of %s', allowed_kinds)
291 291
292 292 if prefix is None:
293 293 prefix = '%s-%s' % (self.repository.name, self.short_id)
294 294 elif prefix.startswith('/'):
295 295 raise VCSError("Prefix cannot start with leading slash")
296 296 elif prefix.strip() == '':
297 297 raise VCSError("Prefix cannot be empty")
298 298
299 299 if kind == 'zip':
300 300 frmt = 'zip'
301 301 else:
302 302 frmt = 'tar'
303 303 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
304 304 self.raw_id)
305 305 if kind == 'tgz':
306 306 cmd += ' | gzip -9'
307 307 elif kind == 'tbz2':
308 308 cmd += ' | bzip2 -9'
309 309
310 310 if stream is None:
311 311 raise VCSError('You need to pass in a valid stream for filling'
312 312 ' with archival data')
313 313 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
314 314 cwd=self.repository.path)
315 315
316 316 buffer_size = 1024 * 8
317 317 chunk = popen.stdout.read(buffer_size)
318 318 while chunk:
319 319 stream.write(chunk)
320 320 chunk = popen.stdout.read(buffer_size)
321 321 # Make sure all descriptors would be read
322 322 popen.communicate()
323 323
324 324 def get_nodes(self, path):
325 325 if self._get_kind(path) != NodeKind.DIR:
326 326 raise ChangesetError("Directory does not exist for revision %r at "
327 327 " %r" % (self.revision, path))
328 328 path = self._fix_path(path)
329 329 id = self._get_id_for_path(path)
330 330 tree = self.repository._repo[id]
331 331 dirnodes = []
332 332 filenodes = []
333 333 als = self.repository.alias
334 334 for name, stat, id in tree.iteritems():
335 335 if objects.S_ISGITLINK(stat):
336 336 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
337 337 alias=als))
338 338 continue
339 339
340 340 obj = self.repository._repo.get_object(id)
341 341 if path != '':
342 342 obj_path = '/'.join((path, name))
343 343 else:
344 344 obj_path = name
345 345 if obj_path not in self._stat_modes:
346 346 self._stat_modes[obj_path] = stat
347 347 if isinstance(obj, objects.Tree):
348 348 dirnodes.append(DirNode(obj_path, changeset=self))
349 349 elif isinstance(obj, objects.Blob):
350 350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
351 351 else:
352 352 raise ChangesetError("Requested object should be Tree "
353 353 "or Blob, is %r" % type(obj))
354 354 nodes = dirnodes + filenodes
355 355 for node in nodes:
356 356 if not node.path in self.nodes:
357 357 self.nodes[node.path] = node
358 358 nodes.sort()
359 359 return nodes
360 360
361 361 def get_node(self, path):
362 362 if isinstance(path, unicode):
363 363 path = path.encode('utf-8')
364 364 path = self._fix_path(path)
365 365 if not path in self.nodes:
366 366 try:
367 367 id_ = self._get_id_for_path(path)
368 368 except ChangesetError:
369 369 raise NodeDoesNotExistError("Cannot find one of parents' "
370 370 "directories for a given path: %s" % path)
371 371
372 372 als = self.repository.alias
373 373 _GL = lambda m: m and objects.S_ISGITLINK(m)
374 374 if _GL(self._stat_modes.get(path)):
375 375 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
376 376 else:
377 377 obj = self.repository._repo.get_object(id_)
378 378
379 379 if isinstance(obj, objects.Tree):
380 380 if path == '':
381 381 node = RootNode(changeset=self)
382 382 else:
383 383 node = DirNode(path, changeset=self)
384 384 node._tree = obj
385 385 elif isinstance(obj, objects.Blob):
386 386 node = FileNode(path, changeset=self)
387 387 node._blob = obj
388 388 else:
389 389 raise NodeDoesNotExistError("There is no file nor directory "
390 390 "at the given path %r at revision %r"
391 391 % (path, self.short_id))
392 392 # cache node
393 393 self.nodes[path] = node
394 394 return self.nodes[path]
395 395
396 396 @LazyProperty
397 397 def affected_files(self):
398 398 """
399 399 Get's a fast accessible file changes for given changeset
400 400 """
401 401
402 402 return self.added + self.changed
403 403
404 404 @LazyProperty
405 405 def _diff_name_status(self):
406 406 output = []
407 407 for parent in self.parents:
408 408 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
409 409 so, se = self.repository.run_git_command(cmd)
410 410 output.append(so.strip())
411 411 return '\n'.join(output)
412 412
413 413 def _get_paths_for_status(self, status):
414 414 """
415 415 Returns sorted list of paths for given ``status``.
416 416
417 417 :param status: one of: *added*, *modified* or *deleted*
418 418 """
419 419 paths = set()
420 420 char = status[0].upper()
421 421 for line in self._diff_name_status.splitlines():
422 422 if not line:
423 423 continue
424 424
425 425 if line.startswith(char):
426 426 splitted = line.split(char, 1)
427 427 if not len(splitted) == 2:
428 428 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
429 429 "particularly that line: %s" % (self._diff_name_status,
430 430 line))
431 431 _path = splitted[1].strip()
432 432 paths.add(_path)
433 433 return sorted(paths)
434 434
435 435 @LazyProperty
436 436 def added(self):
437 437 """
438 438 Returns list of added ``FileNode`` objects.
439 439 """
440 440 if not self.parents:
441 441 return list(self._get_file_nodes())
442 442 return [self.get_node(path) for path in self._get_paths_for_status('added')]
443 443
444 444 @LazyProperty
445 445 def changed(self):
446 446 """
447 447 Returns list of modified ``FileNode`` objects.
448 448 """
449 449 if not self.parents:
450 450 return []
451 451 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
452 452
453 453 @LazyProperty
454 454 def removed(self):
455 455 """
456 456 Returns list of removed ``FileNode`` objects.
457 457 """
458 458 if not self.parents:
459 459 return []
460 460 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,355 +1,357 b''
1 1 import os
2 2 import posixpath
3 3
4 4 from rhodecode.lib.vcs.backends.base import BaseChangeset
5 5 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
7 7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
8 8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
9 9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
10 10 RemovedFileNodesGenerator, RootNode, SubModuleNode
11 11
12 12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
13 13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
14 14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
15 15
16 16 from ...utils.hgcompat import archival, hex
17 17
18 18
19 19 class MercurialChangeset(BaseChangeset):
20 20 """
21 21 Represents state of the repository at the single revision.
22 22 """
23 23
24 24 def __init__(self, repository, revision):
25 25 self.repository = repository
26 26 self.raw_id = revision
27 27 self._ctx = repository._repo[revision]
28 28 self.revision = self._ctx._rev
29 29 self.nodes = {}
30 30
31 31 @LazyProperty
32 32 def tags(self):
33 33 return map(safe_unicode, self._ctx.tags())
34 34
35 35 @LazyProperty
36 36 def branch(self):
37 37 return safe_unicode(self._ctx.branch())
38 38
39 39 @LazyProperty
40 def bookmarks(self):
41 return map(safe_unicode, self._ctx.bookmarks())
42
43 @LazyProperty
40 44 def message(self):
41 45 return safe_unicode(self._ctx.description())
42 46
43 47 @LazyProperty
44 48 def author(self):
45 49 return safe_unicode(self._ctx.user())
46 50
47 51 @LazyProperty
48 52 def date(self):
49 53 return date_fromtimestamp(*self._ctx.date())
50 54
51 55 @LazyProperty
52 56 def status(self):
53 57 """
54 58 Returns modified, added, removed, deleted files for current changeset
55 59 """
56 60 return self.repository._repo.status(self._ctx.p1().node(),
57 61 self._ctx.node())
58 62
59 63 @LazyProperty
60 64 def _file_paths(self):
61 65 return list(self._ctx)
62 66
63 67 @LazyProperty
64 68 def _dir_paths(self):
65 69 p = list(set(get_dirs_for_path(*self._file_paths)))
66 70 p.insert(0, '')
67 71 return p
68 72
69 73 @LazyProperty
70 74 def _paths(self):
71 75 return self._dir_paths + self._file_paths
72 76
73 77 @LazyProperty
74 78 def id(self):
75 79 if self.last:
76 80 return u'tip'
77 81 return self.short_id
78 82
79 83 @LazyProperty
80 84 def short_id(self):
81 85 return self.raw_id[:12]
82 86
83 87 @LazyProperty
84 88 def parents(self):
85 89 """
86 90 Returns list of parents changesets.
87 91 """
88 92 return [self.repository.get_changeset(parent.rev())
89 93 for parent in self._ctx.parents() if parent.rev() >= 0]
90 94
91 95 def next(self, branch=None):
92 96
93 97 if branch and self.branch != branch:
94 98 raise VCSError('Branch option used on changeset not belonging '
95 99 'to that branch')
96 100
97 101 def _next(changeset, branch):
98 102 try:
99 103 next_ = changeset.revision + 1
100 104 next_rev = changeset.repository.revisions[next_]
101 105 except IndexError:
102 106 raise ChangesetDoesNotExistError
103 107 cs = changeset.repository.get_changeset(next_rev)
104 108
105 109 if branch and branch != cs.branch:
106 110 return _next(cs, branch)
107 111
108 112 return cs
109 113
110 114 return _next(self, branch)
111 115
112 116 def prev(self, branch=None):
113 117 if branch and self.branch != branch:
114 118 raise VCSError('Branch option used on changeset not belonging '
115 119 'to that branch')
116 120
117 121 def _prev(changeset, branch):
118 122 try:
119 123 prev_ = changeset.revision - 1
120 124 if prev_ < 0:
121 125 raise IndexError
122 126 prev_rev = changeset.repository.revisions[prev_]
123 127 except IndexError:
124 128 raise ChangesetDoesNotExistError
125 129
126 130 cs = changeset.repository.get_changeset(prev_rev)
127 131
128 132 if branch and branch != cs.branch:
129 133 return _prev(cs, branch)
130 134
131 135 return cs
132 136
133 137 return _prev(self, branch)
134 138
135 139 def _fix_path(self, path):
136 140 """
137 141 Paths are stored without trailing slash so we need to get rid off it if
138 142 needed. Also mercurial keeps filenodes as str so we need to decode
139 143 from unicode to str
140 144 """
141 145 if path.endswith('/'):
142 146 path = path.rstrip('/')
143 147
144 148 return safe_str(path)
145 149
146 150 def _get_kind(self, path):
147 151 path = self._fix_path(path)
148 152 if path in self._file_paths:
149 153 return NodeKind.FILE
150 154 elif path in self._dir_paths:
151 155 return NodeKind.DIR
152 156 else:
153 157 raise ChangesetError("Node does not exist at the given path %r"
154 158 % (path))
155 159
156 160 def _get_filectx(self, path):
157 161 path = self._fix_path(path)
158 162 if self._get_kind(path) != NodeKind.FILE:
159 163 raise ChangesetError("File does not exist for revision %r at "
160 164 " %r" % (self.revision, path))
161 165 return self._ctx.filectx(path)
162 166
163 167 def _extract_submodules(self):
164 168 """
165 169 returns a dictionary with submodule information from substate file
166 170 of hg repository
167 171 """
168 172 return self._ctx.substate
169 173
170 174 def get_file_mode(self, path):
171 175 """
172 176 Returns stat mode of the file at the given ``path``.
173 177 """
174 178 fctx = self._get_filectx(path)
175 179 if 'x' in fctx.flags():
176 180 return 0100755
177 181 else:
178 182 return 0100644
179 183
180 184 def get_file_content(self, path):
181 185 """
182 186 Returns content of the file at given ``path``.
183 187 """
184 188 fctx = self._get_filectx(path)
185 189 return fctx.data()
186 190
187 191 def get_file_size(self, path):
188 192 """
189 193 Returns size of the file at given ``path``.
190 194 """
191 195 fctx = self._get_filectx(path)
192 196 return fctx.size()
193 197
194 198 def get_file_changeset(self, path):
195 199 """
196 200 Returns last commit of the file at the given ``path``.
197 201 """
198 202 node = self.get_node(path)
199 203 return node.history[0]
200 204
201 205 def get_file_history(self, path):
202 206 """
203 207 Returns history of file as reversed list of ``Changeset`` objects for
204 208 which file at given ``path`` has been modified.
205 209 """
206 210 fctx = self._get_filectx(path)
207 211 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
208 212 changesets = [self.repository.get_changeset(hex(node))
209 213 for node in reversed(nodes)]
210 214 return changesets
211 215
212 216 def get_file_annotate(self, path):
213 217 """
214 218 Returns a list of three element tuples with lineno,changeset and line
215 219 """
216 220 fctx = self._get_filectx(path)
217 221 annotate = []
218 222 for i, annotate_data in enumerate(fctx.annotate()):
219 223 ln_no = i + 1
220 224 annotate.append((ln_no, self.repository\
221 225 .get_changeset(hex(annotate_data[0].node())),
222 226 annotate_data[1],))
223 227
224 228 return annotate
225 229
226 230 def fill_archive(self, stream=None, kind='tgz', prefix=None,
227 231 subrepos=False):
228 232 """
229 233 Fills up given stream.
230 234
231 235 :param stream: file like object.
232 236 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
233 237 Default: ``tgz``.
234 238 :param prefix: name of root directory in archive.
235 239 Default is repository name and changeset's raw_id joined with dash
236 240 (``repo-tip.<KIND>``).
237 241 :param subrepos: include subrepos in this archive.
238 242
239 243 :raise ImproperArchiveTypeError: If given kind is wrong.
240 244 :raise VcsError: If given stream is None
241 245 """
242 246
243 247 allowed_kinds = settings.ARCHIVE_SPECS.keys()
244 248 if kind not in allowed_kinds:
245 249 raise ImproperArchiveTypeError('Archive kind not supported use one'
246 250 'of %s', allowed_kinds)
247 251
248 252 if stream is None:
249 253 raise VCSError('You need to pass in a valid stream for filling'
250 254 ' with archival data')
251 255
252 256 if prefix is None:
253 257 prefix = '%s-%s' % (self.repository.name, self.short_id)
254 258 elif prefix.startswith('/'):
255 259 raise VCSError("Prefix cannot start with leading slash")
256 260 elif prefix.strip() == '':
257 261 raise VCSError("Prefix cannot be empty")
258 262
259 263 archival.archive(self.repository._repo, stream, self.raw_id,
260 264 kind, prefix=prefix, subrepos=subrepos)
261 265
262 #stream.close()
263
264 266 if stream.closed and hasattr(stream, 'name'):
265 267 stream = open(stream.name, 'rb')
266 268 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
267 269 stream = open(stream.name, 'rb')
268 270 else:
269 271 stream.seek(0)
270 272
271 273 def get_nodes(self, path):
272 274 """
273 275 Returns combined ``DirNode`` and ``FileNode`` objects list representing
274 276 state of changeset at the given ``path``. If node at the given ``path``
275 277 is not instance of ``DirNode``, ChangesetError would be raised.
276 278 """
277 279
278 280 if self._get_kind(path) != NodeKind.DIR:
279 281 raise ChangesetError("Directory does not exist for revision %r at "
280 282 " %r" % (self.revision, path))
281 283 path = self._fix_path(path)
282 284
283 285 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
284 286 if os.path.dirname(f) == path]
285 287 dirs = path == '' and '' or [d for d in self._dir_paths
286 288 if d and posixpath.dirname(d) == path]
287 289 dirnodes = [DirNode(d, changeset=self) for d in dirs
288 290 if os.path.dirname(d) == path]
289 291
290 292 als = self.repository.alias
291 293 for k, vals in self._extract_submodules().iteritems():
292 294 #vals = url,rev,type
293 295 loc = vals[0]
294 296 cs = vals[1]
295 297 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
296 298 alias=als))
297 299 nodes = dirnodes + filenodes
298 300 # cache nodes
299 301 for node in nodes:
300 302 self.nodes[node.path] = node
301 303 nodes.sort()
302 304
303 305 return nodes
304 306
305 307 def get_node(self, path):
306 308 """
307 309 Returns ``Node`` object from the given ``path``. If there is no node at
308 310 the given ``path``, ``ChangesetError`` would be raised.
309 311 """
310 312
311 313 path = self._fix_path(path)
312 314
313 315 if not path in self.nodes:
314 316 if path in self._file_paths:
315 317 node = FileNode(path, changeset=self)
316 318 elif path in self._dir_paths or path in self._dir_paths:
317 319 if path == '':
318 320 node = RootNode(changeset=self)
319 321 else:
320 322 node = DirNode(path, changeset=self)
321 323 else:
322 324 raise NodeDoesNotExistError("There is no file nor directory "
323 325 "at the given path: %r at revision %r"
324 326 % (path, self.short_id))
325 327 # cache node
326 328 self.nodes[path] = node
327 329 return self.nodes[path]
328 330
329 331 @LazyProperty
330 332 def affected_files(self):
331 333 """
332 334 Get's a fast accessible file changes for given changeset
333 335 """
334 336 return self._ctx.files()
335 337
336 338 @property
337 339 def added(self):
338 340 """
339 341 Returns list of added ``FileNode`` objects.
340 342 """
341 343 return AddedFileNodesGenerator([n for n in self.status[1]], self)
342 344
343 345 @property
344 346 def changed(self):
345 347 """
346 348 Returns list of modified ``FileNode`` objects.
347 349 """
348 350 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
349 351
350 352 @property
351 353 def removed(self):
352 354 """
353 355 Returns list of removed ``FileNode`` objects.
354 356 """
355 357 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,139 +1,138 b''
1 1 """
2 2 This module provides some useful tools for ``vcs`` like annotate/diff html
3 3 output. It also includes some internal helpers.
4 4 """
5 5 import sys
6 6 import time
7 7 import datetime
8 8
9 9
10 10 def makedate():
11 11 lt = time.localtime()
12 12 if lt[8] == 1 and time.daylight:
13 13 tz = time.altzone
14 14 else:
15 15 tz = time.timezone
16 16 return time.mktime(lt), tz
17 17
18 18
19 19 def date_fromtimestamp(unixts, tzoffset=0):
20 20 """
21 21 Makes a local datetime object out of unix timestamp
22 22
23 23 :param unixts:
24 24 :param tzoffset:
25 25 """
26 26
27 27 return datetime.datetime.fromtimestamp(float(unixts))
28 28
29 29
30 30 def safe_unicode(str_, from_encoding=None):
31 31 """
32 32 safe unicode function. Does few trick to turn str_ into unicode
33 33
34 34 In case of UnicodeDecode error we try to return it with encoding detected
35 35 by chardet library if it fails fallback to unicode with errors replaced
36 36
37 37 :param str_: string to decode
38 38 :rtype: unicode
39 39 :returns: unicode object
40 40 """
41 41 if isinstance(str_, unicode):
42 42 return str_
43 43 if not from_encoding:
44 44 import rhodecode
45 45 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
46 46 from_encoding = DEFAULT_ENCODING
47 47 try:
48 48 return unicode(str_)
49 49 except UnicodeDecodeError:
50 50 pass
51 51
52 52 try:
53 53 return unicode(str_, from_encoding)
54 54 except UnicodeDecodeError:
55 55 pass
56 56
57 57 try:
58 58 import chardet
59 59 encoding = chardet.detect(str_)['encoding']
60 60 if encoding is None:
61 61 raise Exception()
62 62 return str_.decode(encoding)
63 63 except (ImportError, UnicodeDecodeError, Exception):
64 64 return unicode(str_, from_encoding, 'replace')
65 65
66 66
67 67 def safe_str(unicode_, to_encoding=None):
68 68 """
69 69 safe str function. Does few trick to turn unicode_ into string
70 70
71 71 In case of UnicodeEncodeError we try to return it with encoding detected
72 72 by chardet library if it fails fallback to string with errors replaced
73 73
74 74 :param unicode_: unicode to encode
75 75 :rtype: str
76 76 :returns: str object
77 77 """
78 78
79 79 if isinstance(unicode_, str):
80 80 return unicode_
81 81 if not to_encoding:
82 82 import rhodecode
83 83 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
84 84 to_encoding = DEFAULT_ENCODING
85 85 try:
86 86 return unicode_.encode(to_encoding)
87 87 except UnicodeEncodeError:
88 88 pass
89 89
90 90 try:
91 91 import chardet
92 92 encoding = chardet.detect(unicode_)['encoding']
93 print encoding
94 93 if encoding is None:
95 94 raise UnicodeEncodeError()
96 95
97 96 return unicode_.encode(encoding)
98 97 except (ImportError, UnicodeEncodeError):
99 98 return unicode_.encode(to_encoding, 'replace')
100 99
101 100 return safe_str
102 101
103 102
104 103 def author_email(author):
105 104 """
106 105 returns email address of given author.
107 106 If any of <,> sign are found, it fallbacks to regex findall()
108 107 and returns first found result or empty string
109 108
110 109 Regex taken from http://www.regular-expressions.info/email.html
111 110 """
112 111 import re
113 112 r = author.find('>')
114 113 l = author.find('<')
115 114
116 115 if l == -1 or r == -1:
117 116 # fallback to regex match of email out of a string
118 117 email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!"""
119 118 r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z"""
120 119 r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]"""
121 120 r"""*[a-z0-9])?""", re.IGNORECASE)
122 121 m = re.findall(email_re, author)
123 122 return m[0] if m else ''
124 123
125 124 return author[l + 1:r].strip()
126 125
127 126
128 127 def author_name(author):
129 128 """
130 129 get name of author, or else username.
131 130 It'll try to find an email in the author string and just cut it off
132 131 to get the username
133 132 """
134 133
135 134 if not '@' in author:
136 135 return author
137 136 else:
138 137 return author.replace(author_email(author), '').replace('<', '')\
139 138 .replace('>', '').strip()
@@ -1,1356 +1,1357 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 from collections import defaultdict
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.ext.hybrid import hybrid_property
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from pylons.i18n.translation import lazy_ugettext as _
38 38
39 39 from rhodecode.lib.vcs import get_backend
40 40 from rhodecode.lib.vcs.utils.helpers import get_scm
41 41 from rhodecode.lib.vcs.exceptions import VCSError
42 42 from rhodecode.lib.vcs.utils.lazy import LazyProperty
43 43
44 44 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
45 45 safe_unicode
46 46 from rhodecode.lib.compat import json
47 47 from rhodecode.lib.caching_query import FromCache
48 48
49 49 from rhodecode.model.meta import Base, Session
50 50 import hashlib
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55 #==============================================================================
56 56 # BASE CLASSES
57 57 #==============================================================================
58 58
59 59 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
60 60
61 61
62 62 class ModelSerializer(json.JSONEncoder):
63 63 """
64 64 Simple Serializer for JSON,
65 65
66 66 usage::
67 67
68 68 to make object customized for serialization implement a __json__
69 69 method that will return a dict for serialization into json
70 70
71 71 example::
72 72
73 73 class Task(object):
74 74
75 75 def __init__(self, name, value):
76 76 self.name = name
77 77 self.value = value
78 78
79 79 def __json__(self):
80 80 return dict(name=self.name,
81 81 value=self.value)
82 82
83 83 """
84 84
85 85 def default(self, obj):
86 86
87 87 if hasattr(obj, '__json__'):
88 88 return obj.__json__()
89 89 else:
90 90 return json.JSONEncoder.default(self, obj)
91 91
92 92
93 93 class BaseModel(object):
94 94 """
95 95 Base Model for all classess
96 96 """
97 97
98 98 @classmethod
99 99 def _get_keys(cls):
100 100 """return column names for this model """
101 101 return class_mapper(cls).c.keys()
102 102
103 103 def get_dict(self):
104 104 """
105 105 return dict with keys and values corresponding
106 106 to this model data """
107 107
108 108 d = {}
109 109 for k in self._get_keys():
110 110 d[k] = getattr(self, k)
111 111
112 112 # also use __json__() if present to get additional fields
113 113 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
114 114 d[k] = val
115 115 return d
116 116
117 117 def get_appstruct(self):
118 118 """return list with keys and values tupples corresponding
119 119 to this model data """
120 120
121 121 l = []
122 122 for k in self._get_keys():
123 123 l.append((k, getattr(self, k),))
124 124 return l
125 125
126 126 def populate_obj(self, populate_dict):
127 127 """populate model with data from given populate_dict"""
128 128
129 129 for k in self._get_keys():
130 130 if k in populate_dict:
131 131 setattr(self, k, populate_dict[k])
132 132
133 133 @classmethod
134 134 def query(cls):
135 135 return Session.query(cls)
136 136
137 137 @classmethod
138 138 def get(cls, id_):
139 139 if id_:
140 140 return cls.query().get(id_)
141 141
142 142 @classmethod
143 143 def getAll(cls):
144 144 return cls.query().all()
145 145
146 146 @classmethod
147 147 def delete(cls, id_):
148 148 obj = cls.query().get(id_)
149 149 Session.delete(obj)
150 150
151 151 def __repr__(self):
152 152 if hasattr(self, '__unicode__'):
153 153 # python repr needs to return str
154 154 return safe_str(self.__unicode__())
155 155 return '<DB:%s>' % (self.__class__.__name__)
156 156
157 157 class RhodeCodeSetting(Base, BaseModel):
158 158 __tablename__ = 'rhodecode_settings'
159 159 __table_args__ = (
160 160 UniqueConstraint('app_settings_name'),
161 161 {'extend_existing': True, 'mysql_engine': 'InnoDB',
162 162 'mysql_charset': 'utf8'}
163 163 )
164 164 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
165 165 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
166 166 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
167 167
168 168 def __init__(self, k='', v=''):
169 169 self.app_settings_name = k
170 170 self.app_settings_value = v
171 171
172 172 @validates('_app_settings_value')
173 173 def validate_settings_value(self, key, val):
174 174 assert type(val) == unicode
175 175 return val
176 176
177 177 @hybrid_property
178 178 def app_settings_value(self):
179 179 v = self._app_settings_value
180 180 if self.app_settings_name == 'ldap_active':
181 181 v = str2bool(v)
182 182 return v
183 183
184 184 @app_settings_value.setter
185 185 def app_settings_value(self, val):
186 186 """
187 187 Setter that will always make sure we use unicode in app_settings_value
188 188
189 189 :param val:
190 190 """
191 191 self._app_settings_value = safe_unicode(val)
192 192
193 193 def __unicode__(self):
194 194 return u"<%s('%s:%s')>" % (
195 195 self.__class__.__name__,
196 196 self.app_settings_name, self.app_settings_value
197 197 )
198 198
199 199 @classmethod
200 200 def get_by_name(cls, ldap_key):
201 201 return cls.query()\
202 202 .filter(cls.app_settings_name == ldap_key).scalar()
203 203
204 204 @classmethod
205 205 def get_app_settings(cls, cache=False):
206 206
207 207 ret = cls.query()
208 208
209 209 if cache:
210 210 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
211 211
212 212 if not ret:
213 213 raise Exception('Could not get application settings !')
214 214 settings = {}
215 215 for each in ret:
216 216 settings['rhodecode_' + each.app_settings_name] = \
217 217 each.app_settings_value
218 218
219 219 return settings
220 220
221 221 @classmethod
222 222 def get_ldap_settings(cls, cache=False):
223 223 ret = cls.query()\
224 224 .filter(cls.app_settings_name.startswith('ldap_')).all()
225 225 fd = {}
226 226 for row in ret:
227 227 fd.update({row.app_settings_name:row.app_settings_value})
228 228
229 229 return fd
230 230
231 231
232 232 class RhodeCodeUi(Base, BaseModel):
233 233 __tablename__ = 'rhodecode_ui'
234 234 __table_args__ = (
235 235 UniqueConstraint('ui_key'),
236 236 {'extend_existing': True, 'mysql_engine': 'InnoDB',
237 237 'mysql_charset': 'utf8'}
238 238 )
239 239
240 240 HOOK_UPDATE = 'changegroup.update'
241 241 HOOK_REPO_SIZE = 'changegroup.repo_size'
242 242 HOOK_PUSH = 'pretxnchangegroup.push_logger'
243 243 HOOK_PULL = 'preoutgoing.pull_logger'
244 244
245 245 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
246 246 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
247 247 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
248 248 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
249 249 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
250 250
251 251 @classmethod
252 252 def get_by_key(cls, key):
253 253 return cls.query().filter(cls.ui_key == key)
254 254
255 255 @classmethod
256 256 def get_builtin_hooks(cls):
257 257 q = cls.query()
258 258 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
259 259 cls.HOOK_REPO_SIZE,
260 260 cls.HOOK_PUSH, cls.HOOK_PULL]))
261 261 return q.all()
262 262
263 263 @classmethod
264 264 def get_custom_hooks(cls):
265 265 q = cls.query()
266 266 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
267 267 cls.HOOK_REPO_SIZE,
268 268 cls.HOOK_PUSH, cls.HOOK_PULL]))
269 269 q = q.filter(cls.ui_section == 'hooks')
270 270 return q.all()
271 271
272 272 @classmethod
273 273 def create_or_update_hook(cls, key, val):
274 274 new_ui = cls.get_by_key(key).scalar() or cls()
275 275 new_ui.ui_section = 'hooks'
276 276 new_ui.ui_active = True
277 277 new_ui.ui_key = key
278 278 new_ui.ui_value = val
279 279
280 280 Session.add(new_ui)
281 281
282 282
283 283 class User(Base, BaseModel):
284 284 __tablename__ = 'users'
285 285 __table_args__ = (
286 286 UniqueConstraint('username'), UniqueConstraint('email'),
287 287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
288 288 'mysql_charset': 'utf8'}
289 289 )
290 290 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
291 291 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
292 292 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
293 293 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
294 294 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
295 295 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
296 296 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
297 297 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
298 298 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
299 299 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
300 300 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
301 301
302 302 user_log = relationship('UserLog', cascade='all')
303 303 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
304 304
305 305 repositories = relationship('Repository')
306 306 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
307 307 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
308 308 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
309 309
310 310 group_member = relationship('UsersGroupMember', cascade='all')
311 311
312 312 notifications = relationship('UserNotification', cascade='all')
313 313 # notifications assigned to this user
314 314 user_created_notifications = relationship('Notification', cascade='all')
315 315 # comments created by this user
316 316 user_comments = relationship('ChangesetComment', cascade='all')
317 317
318 318 @hybrid_property
319 319 def email(self):
320 320 return self._email
321 321
322 322 @email.setter
323 323 def email(self, val):
324 324 self._email = val.lower() if val else None
325 325
326 326 @property
327 327 def full_name(self):
328 328 return '%s %s' % (self.name, self.lastname)
329 329
330 330 @property
331 331 def full_name_or_username(self):
332 332 return ('%s %s' % (self.name, self.lastname)
333 333 if (self.name and self.lastname) else self.username)
334 334
335 335 @property
336 336 def full_contact(self):
337 337 return '%s %s <%s>' % (self.name, self.lastname, self.email)
338 338
339 339 @property
340 340 def short_contact(self):
341 341 return '%s %s' % (self.name, self.lastname)
342 342
343 343 @property
344 344 def is_admin(self):
345 345 return self.admin
346 346
347 347 def __unicode__(self):
348 348 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
349 349 self.user_id, self.username)
350 350
351 351 @classmethod
352 352 def get_by_username(cls, username, case_insensitive=False, cache=False):
353 353 if case_insensitive:
354 354 q = cls.query().filter(cls.username.ilike(username))
355 355 else:
356 356 q = cls.query().filter(cls.username == username)
357 357
358 358 if cache:
359 359 q = q.options(FromCache(
360 360 "sql_cache_short",
361 361 "get_user_%s" % _hash_key(username)
362 362 )
363 363 )
364 364 return q.scalar()
365 365
366 366 @classmethod
367 367 def get_by_api_key(cls, api_key, cache=False):
368 368 q = cls.query().filter(cls.api_key == api_key)
369 369
370 370 if cache:
371 371 q = q.options(FromCache("sql_cache_short",
372 372 "get_api_key_%s" % api_key))
373 373 return q.scalar()
374 374
375 375 @classmethod
376 376 def get_by_email(cls, email, case_insensitive=False, cache=False):
377 377 if case_insensitive:
378 378 q = cls.query().filter(cls.email.ilike(email))
379 379 else:
380 380 q = cls.query().filter(cls.email == email)
381 381
382 382 if cache:
383 383 q = q.options(FromCache("sql_cache_short",
384 384 "get_api_key_%s" % email))
385 385 return q.scalar()
386 386
387 387 def update_lastlogin(self):
388 388 """Update user lastlogin"""
389 389 self.last_login = datetime.datetime.now()
390 390 Session.add(self)
391 391 log.debug('updated user %s lastlogin' % self.username)
392 392
393 393 def __json__(self):
394 394 return dict(
395 395 user_id=self.user_id,
396 396 first_name=self.name,
397 397 last_name=self.lastname,
398 398 email=self.email,
399 399 full_name=self.full_name,
400 400 full_name_or_username=self.full_name_or_username,
401 401 short_contact=self.short_contact,
402 402 full_contact=self.full_contact
403 403 )
404 404
405 405
406 406 class UserLog(Base, BaseModel):
407 407 __tablename__ = 'user_logs'
408 408 __table_args__ = (
409 409 {'extend_existing': True, 'mysql_engine': 'InnoDB',
410 410 'mysql_charset': 'utf8'},
411 411 )
412 412 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
413 413 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
414 414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
415 415 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
416 416 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
417 417 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
418 418 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
419 419
420 420 @property
421 421 def action_as_day(self):
422 422 return datetime.date(*self.action_date.timetuple()[:3])
423 423
424 424 user = relationship('User')
425 425 repository = relationship('Repository', cascade='')
426 426
427 427
428 428 class UsersGroup(Base, BaseModel):
429 429 __tablename__ = 'users_groups'
430 430 __table_args__ = (
431 431 {'extend_existing': True, 'mysql_engine': 'InnoDB',
432 432 'mysql_charset': 'utf8'},
433 433 )
434 434
435 435 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
436 436 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
437 437 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
438 438
439 439 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
440 440 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
441 441 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
442 442
443 443 def __unicode__(self):
444 444 return u'<userGroup(%s)>' % (self.users_group_name)
445 445
446 446 @classmethod
447 447 def get_by_group_name(cls, group_name, cache=False,
448 448 case_insensitive=False):
449 449 if case_insensitive:
450 450 q = cls.query().filter(cls.users_group_name.ilike(group_name))
451 451 else:
452 452 q = cls.query().filter(cls.users_group_name == group_name)
453 453 if cache:
454 454 q = q.options(FromCache(
455 455 "sql_cache_short",
456 456 "get_user_%s" % _hash_key(group_name)
457 457 )
458 458 )
459 459 return q.scalar()
460 460
461 461 @classmethod
462 462 def get(cls, users_group_id, cache=False):
463 463 users_group = cls.query()
464 464 if cache:
465 465 users_group = users_group.options(FromCache("sql_cache_short",
466 466 "get_users_group_%s" % users_group_id))
467 467 return users_group.get(users_group_id)
468 468
469 469
470 470 class UsersGroupMember(Base, BaseModel):
471 471 __tablename__ = 'users_groups_members'
472 472 __table_args__ = (
473 473 {'extend_existing': True, 'mysql_engine': 'InnoDB',
474 474 'mysql_charset': 'utf8'},
475 475 )
476 476
477 477 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
478 478 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
479 479 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
480 480
481 481 user = relationship('User', lazy='joined')
482 482 users_group = relationship('UsersGroup')
483 483
484 484 def __init__(self, gr_id='', u_id=''):
485 485 self.users_group_id = gr_id
486 486 self.user_id = u_id
487 487
488 488
489 489 class Repository(Base, BaseModel):
490 490 __tablename__ = 'repositories'
491 491 __table_args__ = (
492 492 UniqueConstraint('repo_name'),
493 493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 494 'mysql_charset': 'utf8'},
495 495 )
496 496
497 497 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
498 498 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
499 499 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
500 500 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
501 501 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
502 502 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
503 503 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
504 504 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
505 505 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
506 506 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
507 507
508 508 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
509 509 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
510 510
511 511 user = relationship('User')
512 512 fork = relationship('Repository', remote_side=repo_id)
513 513 group = relationship('RepoGroup')
514 514 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
515 515 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
516 516 stats = relationship('Statistics', cascade='all', uselist=False)
517 517
518 518 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
519 519
520 520 logs = relationship('UserLog')
521 521
522 522 def __unicode__(self):
523 523 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
524 524 self.repo_name)
525 525
526 526 @classmethod
527 527 def url_sep(cls):
528 528 return '/'
529 529
530 530 @classmethod
531 531 def get_by_repo_name(cls, repo_name):
532 532 q = Session.query(cls).filter(cls.repo_name == repo_name)
533 533 q = q.options(joinedload(Repository.fork))\
534 534 .options(joinedload(Repository.user))\
535 535 .options(joinedload(Repository.group))
536 536 return q.scalar()
537 537
538 538 @classmethod
539 539 def get_repo_forks(cls, repo_id):
540 540 return cls.query().filter(Repository.fork_id == repo_id)
541 541
542 542 @classmethod
543 543 def base_path(cls):
544 544 """
545 545 Returns base path when all repos are stored
546 546
547 547 :param cls:
548 548 """
549 549 q = Session.query(RhodeCodeUi)\
550 550 .filter(RhodeCodeUi.ui_key == cls.url_sep())
551 551 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
552 552 return q.one().ui_value
553 553
554 554 @property
555 555 def just_name(self):
556 556 return self.repo_name.split(Repository.url_sep())[-1]
557 557
558 558 @property
559 559 def groups_with_parents(self):
560 560 groups = []
561 561 if self.group is None:
562 562 return groups
563 563
564 564 cur_gr = self.group
565 565 groups.insert(0, cur_gr)
566 566 while 1:
567 567 gr = getattr(cur_gr, 'parent_group', None)
568 568 cur_gr = cur_gr.parent_group
569 569 if gr is None:
570 570 break
571 571 groups.insert(0, gr)
572 572
573 573 return groups
574 574
575 575 @property
576 576 def groups_and_repo(self):
577 577 return self.groups_with_parents, self.just_name
578 578
579 579 @LazyProperty
580 580 def repo_path(self):
581 581 """
582 582 Returns base full path for that repository means where it actually
583 583 exists on a filesystem
584 584 """
585 585 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
586 586 Repository.url_sep())
587 587 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
588 588 return q.one().ui_value
589 589
590 590 @property
591 591 def repo_full_path(self):
592 592 p = [self.repo_path]
593 593 # we need to split the name by / since this is how we store the
594 594 # names in the database, but that eventually needs to be converted
595 595 # into a valid system path
596 596 p += self.repo_name.split(Repository.url_sep())
597 597 return os.path.join(*p)
598 598
599 599 def get_new_name(self, repo_name):
600 600 """
601 601 returns new full repository name based on assigned group and new new
602 602
603 603 :param group_name:
604 604 """
605 605 path_prefix = self.group.full_path_splitted if self.group else []
606 606 return Repository.url_sep().join(path_prefix + [repo_name])
607 607
608 608 @property
609 609 def _ui(self):
610 610 """
611 611 Creates an db based ui object for this repository
612 612 """
613 613 from mercurial import ui
614 614 from mercurial import config
615 615 baseui = ui.ui()
616 616
617 617 #clean the baseui object
618 618 baseui._ocfg = config.config()
619 619 baseui._ucfg = config.config()
620 620 baseui._tcfg = config.config()
621 621
622 622 ret = RhodeCodeUi.query()\
623 623 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
624 624
625 625 hg_ui = ret
626 626 for ui_ in hg_ui:
627 627 if ui_.ui_active:
628 628 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
629 629 ui_.ui_key, ui_.ui_value)
630 630 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
631 631
632 632 return baseui
633 633
634 634 @classmethod
635 635 def is_valid(cls, repo_name):
636 636 """
637 637 returns True if given repo name is a valid filesystem repository
638 638
639 639 :param cls:
640 640 :param repo_name:
641 641 """
642 642 from rhodecode.lib.utils import is_valid_repo
643 643
644 644 return is_valid_repo(repo_name, cls.base_path())
645 645
646 646 #==========================================================================
647 647 # SCM PROPERTIES
648 648 #==========================================================================
649 649
650 def get_changeset(self, rev):
650 def get_changeset(self, rev=None):
651 651 return get_changeset_safe(self.scm_instance, rev)
652 652
653 653 @property
654 654 def tip(self):
655 655 return self.get_changeset('tip')
656 656
657 657 @property
658 658 def author(self):
659 659 return self.tip.author
660 660
661 661 @property
662 662 def last_change(self):
663 663 return self.scm_instance.last_change
664 664
665 665 def comments(self, revisions=None):
666 666 """
667 667 Returns comments for this repository grouped by revisions
668 668
669 669 :param revisions: filter query by revisions only
670 670 """
671 671 cmts = ChangesetComment.query()\
672 672 .filter(ChangesetComment.repo == self)
673 673 if revisions:
674 674 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
675 675 grouped = defaultdict(list)
676 676 for cmt in cmts.all():
677 677 grouped[cmt.revision].append(cmt)
678 678 return grouped
679 679
680 680 def statuses(self, revisions=None):
681 681 """
682 682 Returns statuses for this repository
683 683
684 684 :param revisions: list of revisions to get statuses for
685 685 :type revisions: list
686 686 """
687 687
688 688 statuses = ChangesetStatus.query()\
689 689 .filter(ChangesetStatus.repo == self)
690 690 if revisions:
691 691 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
692 692 grouped = {}
693 693 for stat in statuses.all():
694 694 grouped[stat.revision] = [str(stat.status), stat.status_lbl]
695 695 return grouped
696 696
697 697 #==========================================================================
698 698 # SCM CACHE INSTANCE
699 699 #==========================================================================
700 700
701 701 @property
702 702 def invalidate(self):
703 703 return CacheInvalidation.invalidate(self.repo_name)
704 704
705 705 def set_invalidate(self):
706 706 """
707 707 set a cache for invalidation for this instance
708 708 """
709 709 CacheInvalidation.set_invalidate(self.repo_name)
710 710
711 711 @LazyProperty
712 712 def scm_instance(self):
713 713 return self.__get_instance()
714 714
715 715 @property
716 716 def scm_instance_cached(self):
717 717 @cache_region('long_term')
718 718 def _c(repo_name):
719 719 return self.__get_instance()
720 720 rn = self.repo_name
721 721 log.debug('Getting cached instance of repo')
722 722 inv = self.invalidate
723 723 if inv is not None:
724 724 region_invalidate(_c, None, rn)
725 725 # update our cache
726 726 CacheInvalidation.set_valid(inv.cache_key)
727 727 return _c(rn)
728 728
729 729 def __get_instance(self):
730 730 repo_full_path = self.repo_full_path
731 731 try:
732 732 alias = get_scm(repo_full_path)[0]
733 733 log.debug('Creating instance of %s repository' % alias)
734 734 backend = get_backend(alias)
735 735 except VCSError:
736 736 log.error(traceback.format_exc())
737 737 log.error('Perhaps this repository is in db and not in '
738 738 'filesystem run rescan repositories with '
739 739 '"destroy old data " option from admin panel')
740 740 return
741 741
742 742 if alias == 'hg':
743 743
744 744 repo = backend(safe_str(repo_full_path), create=False,
745 745 baseui=self._ui)
746 746 # skip hidden web repository
747 747 if repo._get_hidden():
748 748 return
749 749 else:
750 750 repo = backend(repo_full_path, create=False)
751 751
752 752 return repo
753 753
754 754
755 755 class RepoGroup(Base, BaseModel):
756 756 __tablename__ = 'groups'
757 757 __table_args__ = (
758 758 UniqueConstraint('group_name', 'group_parent_id'),
759 759 CheckConstraint('group_id != group_parent_id'),
760 760 {'extend_existing': True, 'mysql_engine': 'InnoDB',
761 761 'mysql_charset': 'utf8'},
762 762 )
763 763 __mapper_args__ = {'order_by': 'group_name'}
764 764
765 765 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
766 766 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
767 767 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
768 768 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
769 769
770 770 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
771 771 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
772 772
773 773 parent_group = relationship('RepoGroup', remote_side=group_id)
774 774
775 775 def __init__(self, group_name='', parent_group=None):
776 776 self.group_name = group_name
777 777 self.parent_group = parent_group
778 778
779 779 def __unicode__(self):
780 780 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
781 781 self.group_name)
782 782
783 783 @classmethod
784 784 def groups_choices(cls):
785 785 from webhelpers.html import literal as _literal
786 786 repo_groups = [('', '')]
787 787 sep = ' &raquo; '
788 788 _name = lambda k: _literal(sep.join(k))
789 789
790 790 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
791 791 for x in cls.query().all()])
792 792
793 793 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
794 794 return repo_groups
795 795
796 796 @classmethod
797 797 def url_sep(cls):
798 798 return '/'
799 799
800 800 @classmethod
801 801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
802 802 if case_insensitive:
803 803 gr = cls.query()\
804 804 .filter(cls.group_name.ilike(group_name))
805 805 else:
806 806 gr = cls.query()\
807 807 .filter(cls.group_name == group_name)
808 808 if cache:
809 809 gr = gr.options(FromCache(
810 810 "sql_cache_short",
811 811 "get_group_%s" % _hash_key(group_name)
812 812 )
813 813 )
814 814 return gr.scalar()
815 815
816 816 @property
817 817 def parents(self):
818 818 parents_recursion_limit = 5
819 819 groups = []
820 820 if self.parent_group is None:
821 821 return groups
822 822 cur_gr = self.parent_group
823 823 groups.insert(0, cur_gr)
824 824 cnt = 0
825 825 while 1:
826 826 cnt += 1
827 827 gr = getattr(cur_gr, 'parent_group', None)
828 828 cur_gr = cur_gr.parent_group
829 829 if gr is None:
830 830 break
831 831 if cnt == parents_recursion_limit:
832 832 # this will prevent accidental infinit loops
833 833 log.error('group nested more than %s' %
834 834 parents_recursion_limit)
835 835 break
836 836
837 837 groups.insert(0, gr)
838 838 return groups
839 839
840 840 @property
841 841 def children(self):
842 842 return RepoGroup.query().filter(RepoGroup.parent_group == self)
843 843
844 844 @property
845 845 def name(self):
846 846 return self.group_name.split(RepoGroup.url_sep())[-1]
847 847
848 848 @property
849 849 def full_path(self):
850 850 return self.group_name
851 851
852 852 @property
853 853 def full_path_splitted(self):
854 854 return self.group_name.split(RepoGroup.url_sep())
855 855
856 856 @property
857 857 def repositories(self):
858 858 return Repository.query()\
859 859 .filter(Repository.group == self)\
860 860 .order_by(Repository.repo_name)
861 861
862 862 @property
863 863 def repositories_recursive_count(self):
864 864 cnt = self.repositories.count()
865 865
866 866 def children_count(group):
867 867 cnt = 0
868 868 for child in group.children:
869 869 cnt += child.repositories.count()
870 870 cnt += children_count(child)
871 871 return cnt
872 872
873 873 return cnt + children_count(self)
874 874
875 875 def get_new_name(self, group_name):
876 876 """
877 877 returns new full group name based on parent and new name
878 878
879 879 :param group_name:
880 880 """
881 881 path_prefix = (self.parent_group.full_path_splitted if
882 882 self.parent_group else [])
883 883 return RepoGroup.url_sep().join(path_prefix + [group_name])
884 884
885 885
886 886 class Permission(Base, BaseModel):
887 887 __tablename__ = 'permissions'
888 888 __table_args__ = (
889 889 {'extend_existing': True, 'mysql_engine': 'InnoDB',
890 890 'mysql_charset': 'utf8'},
891 891 )
892 892 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
893 893 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
894 894 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
895 895
896 896 def __unicode__(self):
897 897 return u"<%s('%s:%s')>" % (
898 898 self.__class__.__name__, self.permission_id, self.permission_name
899 899 )
900 900
901 901 @classmethod
902 902 def get_by_key(cls, key):
903 903 return cls.query().filter(cls.permission_name == key).scalar()
904 904
905 905 @classmethod
906 906 def get_default_perms(cls, default_user_id):
907 907 q = Session.query(UserRepoToPerm, Repository, cls)\
908 908 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
909 909 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
910 910 .filter(UserRepoToPerm.user_id == default_user_id)
911 911
912 912 return q.all()
913 913
914 914 @classmethod
915 915 def get_default_group_perms(cls, default_user_id):
916 916 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
917 917 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
918 918 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
919 919 .filter(UserRepoGroupToPerm.user_id == default_user_id)
920 920
921 921 return q.all()
922 922
923 923
924 924 class UserRepoToPerm(Base, BaseModel):
925 925 __tablename__ = 'repo_to_perm'
926 926 __table_args__ = (
927 927 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
928 928 {'extend_existing': True, 'mysql_engine': 'InnoDB',
929 929 'mysql_charset': 'utf8'}
930 930 )
931 931 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
932 932 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
933 933 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
934 934 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
935 935
936 936 user = relationship('User')
937 937 repository = relationship('Repository')
938 938 permission = relationship('Permission')
939 939
940 940 @classmethod
941 941 def create(cls, user, repository, permission):
942 942 n = cls()
943 943 n.user = user
944 944 n.repository = repository
945 945 n.permission = permission
946 946 Session.add(n)
947 947 return n
948 948
949 949 def __unicode__(self):
950 950 return u'<user:%s => %s >' % (self.user, self.repository)
951 951
952 952
953 953 class UserToPerm(Base, BaseModel):
954 954 __tablename__ = 'user_to_perm'
955 955 __table_args__ = (
956 956 UniqueConstraint('user_id', 'permission_id'),
957 957 {'extend_existing': True, 'mysql_engine': 'InnoDB',
958 958 'mysql_charset': 'utf8'}
959 959 )
960 960 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
961 961 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
962 962 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
963 963
964 964 user = relationship('User')
965 965 permission = relationship('Permission', lazy='joined')
966 966
967 967
968 968 class UsersGroupRepoToPerm(Base, BaseModel):
969 969 __tablename__ = 'users_group_repo_to_perm'
970 970 __table_args__ = (
971 971 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
972 972 {'extend_existing': True, 'mysql_engine': 'InnoDB',
973 973 'mysql_charset': 'utf8'}
974 974 )
975 975 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
976 976 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
977 977 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
978 978 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
979 979
980 980 users_group = relationship('UsersGroup')
981 981 permission = relationship('Permission')
982 982 repository = relationship('Repository')
983 983
984 984 @classmethod
985 985 def create(cls, users_group, repository, permission):
986 986 n = cls()
987 987 n.users_group = users_group
988 988 n.repository = repository
989 989 n.permission = permission
990 990 Session.add(n)
991 991 return n
992 992
993 993 def __unicode__(self):
994 994 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
995 995
996 996
997 997 class UsersGroupToPerm(Base, BaseModel):
998 998 __tablename__ = 'users_group_to_perm'
999 999 __table_args__ = (
1000 1000 UniqueConstraint('users_group_id', 'permission_id',),
1001 1001 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1002 1002 'mysql_charset': 'utf8'}
1003 1003 )
1004 1004 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1005 1005 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1006 1006 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1007 1007
1008 1008 users_group = relationship('UsersGroup')
1009 1009 permission = relationship('Permission')
1010 1010
1011 1011
1012 1012 class UserRepoGroupToPerm(Base, BaseModel):
1013 1013 __tablename__ = 'user_repo_group_to_perm'
1014 1014 __table_args__ = (
1015 1015 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1016 1016 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1017 1017 'mysql_charset': 'utf8'}
1018 1018 )
1019 1019
1020 1020 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1021 1021 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1022 1022 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1023 1023 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1024 1024
1025 1025 user = relationship('User')
1026 1026 group = relationship('RepoGroup')
1027 1027 permission = relationship('Permission')
1028 1028
1029 1029
1030 1030 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1031 1031 __tablename__ = 'users_group_repo_group_to_perm'
1032 1032 __table_args__ = (
1033 1033 UniqueConstraint('users_group_id', 'group_id'),
1034 1034 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1035 1035 'mysql_charset': 'utf8'}
1036 1036 )
1037 1037
1038 1038 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1039 1039 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1040 1040 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1041 1041 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1042 1042
1043 1043 users_group = relationship('UsersGroup')
1044 1044 permission = relationship('Permission')
1045 1045 group = relationship('RepoGroup')
1046 1046
1047 1047
1048 1048 class Statistics(Base, BaseModel):
1049 1049 __tablename__ = 'statistics'
1050 1050 __table_args__ = (
1051 1051 UniqueConstraint('repository_id'),
1052 1052 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1053 1053 'mysql_charset': 'utf8'}
1054 1054 )
1055 1055 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1056 1056 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1057 1057 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1058 1058 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1059 1059 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1060 1060 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1061 1061
1062 1062 repository = relationship('Repository', single_parent=True)
1063 1063
1064 1064
1065 1065 class UserFollowing(Base, BaseModel):
1066 1066 __tablename__ = 'user_followings'
1067 1067 __table_args__ = (
1068 1068 UniqueConstraint('user_id', 'follows_repository_id'),
1069 1069 UniqueConstraint('user_id', 'follows_user_id'),
1070 1070 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1071 1071 'mysql_charset': 'utf8'}
1072 1072 )
1073 1073
1074 1074 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1075 1075 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1076 1076 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1077 1077 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1078 1078 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1079 1079
1080 1080 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1081 1081
1082 1082 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1083 1083 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1084 1084
1085 1085 @classmethod
1086 1086 def get_repo_followers(cls, repo_id):
1087 1087 return cls.query().filter(cls.follows_repo_id == repo_id)
1088 1088
1089 1089
1090 1090 class CacheInvalidation(Base, BaseModel):
1091 1091 __tablename__ = 'cache_invalidation'
1092 1092 __table_args__ = (
1093 1093 UniqueConstraint('cache_key'),
1094 1094 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1095 1095 'mysql_charset': 'utf8'},
1096 1096 )
1097 1097 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1098 1098 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1099 1099 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1100 1100 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1101 1101
1102 1102 def __init__(self, cache_key, cache_args=''):
1103 1103 self.cache_key = cache_key
1104 1104 self.cache_args = cache_args
1105 1105 self.cache_active = False
1106 1106
1107 1107 def __unicode__(self):
1108 1108 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1109 1109 self.cache_id, self.cache_key)
1110 1110 @classmethod
1111 1111 def clear_cache(cls):
1112 1112 cls.query().delete()
1113 1113
1114 1114 @classmethod
1115 1115 def _get_key(cls, key):
1116 1116 """
1117 1117 Wrapper for generating a key, together with a prefix
1118 1118
1119 1119 :param key:
1120 1120 """
1121 1121 import rhodecode
1122 1122 prefix = ''
1123 1123 iid = rhodecode.CONFIG.get('instance_id')
1124 1124 if iid:
1125 1125 prefix = iid
1126 1126 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1127 1127
1128 1128 @classmethod
1129 1129 def get_by_key(cls, key):
1130 1130 return cls.query().filter(cls.cache_key == key).scalar()
1131 1131
1132 1132 @classmethod
1133 1133 def _get_or_create_key(cls, key, prefix, org_key):
1134 1134 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1135 1135 if not inv_obj:
1136 1136 try:
1137 1137 inv_obj = CacheInvalidation(key, org_key)
1138 1138 Session.add(inv_obj)
1139 1139 Session.commit()
1140 1140 except Exception:
1141 1141 log.error(traceback.format_exc())
1142 1142 Session.rollback()
1143 1143 return inv_obj
1144 1144
1145 1145 @classmethod
1146 1146 def invalidate(cls, key):
1147 1147 """
1148 1148 Returns Invalidation object if this given key should be invalidated
1149 1149 None otherwise. `cache_active = False` means that this cache
1150 1150 state is not valid and needs to be invalidated
1151 1151
1152 1152 :param key:
1153 1153 """
1154 1154
1155 1155 key, _prefix, _org_key = cls._get_key(key)
1156 1156 inv = cls._get_or_create_key(key, _prefix, _org_key)
1157 1157
1158 1158 if inv and inv.cache_active is False:
1159 1159 return inv
1160 1160
1161 1161 @classmethod
1162 1162 def set_invalidate(cls, key):
1163 1163 """
1164 1164 Mark this Cache key for invalidation
1165 1165
1166 1166 :param key:
1167 1167 """
1168 1168
1169 1169 key, _prefix, _org_key = cls._get_key(key)
1170 1170 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1171 1171 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1172 1172 _org_key))
1173 1173 try:
1174 1174 for inv_obj in inv_objs:
1175 1175 if inv_obj:
1176 1176 inv_obj.cache_active = False
1177 1177
1178 1178 Session.add(inv_obj)
1179 1179 Session.commit()
1180 1180 except Exception:
1181 1181 log.error(traceback.format_exc())
1182 1182 Session.rollback()
1183 1183
1184 1184 @classmethod
1185 1185 def set_valid(cls, key):
1186 1186 """
1187 1187 Mark this cache key as active and currently cached
1188 1188
1189 1189 :param key:
1190 1190 """
1191 1191 inv_obj = cls.get_by_key(key)
1192 1192 inv_obj.cache_active = True
1193 1193 Session.add(inv_obj)
1194 1194 Session.commit()
1195 1195
1196 1196
1197 1197 class ChangesetComment(Base, BaseModel):
1198 1198 __tablename__ = 'changeset_comments'
1199 1199 __table_args__ = (
1200 1200 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1201 1201 'mysql_charset': 'utf8'},
1202 1202 )
1203 1203 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1204 1204 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1205 1205 revision = Column('revision', String(40), nullable=False)
1206 1206 line_no = Column('line_no', Unicode(10), nullable=True)
1207 1207 f_path = Column('f_path', Unicode(1000), nullable=True)
1208 1208 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1209 1209 text = Column('text', Unicode(25000), nullable=False)
1210 1210 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1211 1211
1212 1212 author = relationship('User', lazy='joined')
1213 1213 repo = relationship('Repository')
1214 1214
1215 1215 @classmethod
1216 1216 def get_users(cls, revision):
1217 1217 """
1218 1218 Returns user associated with this changesetComment. ie those
1219 1219 who actually commented
1220 1220
1221 1221 :param cls:
1222 1222 :param revision:
1223 1223 """
1224 1224 return Session.query(User)\
1225 1225 .filter(cls.revision == revision)\
1226 1226 .join(ChangesetComment.author).all()
1227 1227
1228 1228
1229 1229 class ChangesetStatus(Base, BaseModel):
1230 1230 __tablename__ = 'changeset_statuses'
1231 1231 __table_args__ = (
1232 1232 UniqueConstraint('repo_id', 'revision'),
1233 1233 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1234 1234 'mysql_charset': 'utf8'}
1235 1235 )
1236 1236
1237 1237 STATUSES = [
1238 1238 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1239 1239 ('approved', _("Approved")),
1240 1240 ('rejected', _("Rejected")),
1241 1241 ('under_review', _("Under Review")),
1242 1242 ]
1243 1243 DEFAULT = STATUSES[0][0]
1244 1244
1245 1245 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1246 1246 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1247 1247 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1248 1248 revision = Column('revision', String(40), nullable=False)
1249 1249 status = Column('status', String(128), nullable=False, default=DEFAULT)
1250 1250 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1251 1251
1252 1252 author = relationship('User', lazy='joined')
1253 1253 repo = relationship('Repository')
1254 1254
1255 1255 @property
1256 1256 def status_lbl(self):
1257 1257 return dict(self.STATUSES).get(self.status)
1258 1258
1259 1259
1260 1260 class ChangesetStatusHistory(Base, BaseModel):
1261 1261 __tablename__ = 'changeset_statuses_history'
1262 1262 __table_args__ = (
1263 1263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1264 1264 'mysql_charset': 'utf8'}
1265 1265 )
1266 1266 #TODO: check if sqla has a nice history table implementation
1267 1267 changeset_status_id = Column('changeset_status_id', Integer(), ForeignKey('changeset_statuses.changeset_status_id'), nullable=False, primary_key=True)
1268 1268 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1269 1269 status = Column('status', String(128), nullable=False)
1270 1270 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1271 1271
1272 1272
1273 1273 class Notification(Base, BaseModel):
1274 1274 __tablename__ = 'notifications'
1275 1275 __table_args__ = (
1276 1276 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1277 1277 'mysql_charset': 'utf8'},
1278 1278 )
1279 1279
1280 1280 TYPE_CHANGESET_COMMENT = u'cs_comment'
1281 1281 TYPE_MESSAGE = u'message'
1282 1282 TYPE_MENTION = u'mention'
1283 1283 TYPE_REGISTRATION = u'registration'
1284 1284 TYPE_PULL_REQUEST = u'pull_request'
1285 1285
1286 1286 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1287 1287 subject = Column('subject', Unicode(512), nullable=True)
1288 1288 body = Column('body', Unicode(50000), nullable=True)
1289 1289 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1290 1290 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1291 1291 type_ = Column('type', Unicode(256))
1292 1292
1293 1293 created_by_user = relationship('User')
1294 1294 notifications_to_users = relationship('UserNotification', lazy='joined',
1295 1295 cascade="all, delete, delete-orphan")
1296 1296
1297 1297 @property
1298 1298 def recipients(self):
1299 1299 return [x.user for x in UserNotification.query()\
1300 .filter(UserNotification.notification == self).all()]
1300 .filter(UserNotification.notification == self)\
1301 .order_by(UserNotification.user).all()]
1301 1302
1302 1303 @classmethod
1303 1304 def create(cls, created_by, subject, body, recipients, type_=None):
1304 1305 if type_ is None:
1305 1306 type_ = Notification.TYPE_MESSAGE
1306 1307
1307 1308 notification = cls()
1308 1309 notification.created_by_user = created_by
1309 1310 notification.subject = subject
1310 1311 notification.body = body
1311 1312 notification.type_ = type_
1312 1313 notification.created_on = datetime.datetime.now()
1313 1314
1314 1315 for u in recipients:
1315 1316 assoc = UserNotification()
1316 1317 assoc.notification = notification
1317 1318 u.notifications.append(assoc)
1318 1319 Session.add(notification)
1319 1320 return notification
1320 1321
1321 1322 @property
1322 1323 def description(self):
1323 1324 from rhodecode.model.notification import NotificationModel
1324 1325 return NotificationModel().make_description(self)
1325 1326
1326 1327
1327 1328 class UserNotification(Base, BaseModel):
1328 1329 __tablename__ = 'user_to_notification'
1329 1330 __table_args__ = (
1330 1331 UniqueConstraint('user_id', 'notification_id'),
1331 1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1332 1333 'mysql_charset': 'utf8'}
1333 1334 )
1334 1335 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1335 1336 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1336 1337 read = Column('read', Boolean, default=False)
1337 1338 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1338 1339
1339 1340 user = relationship('User', lazy="joined")
1340 1341 notification = relationship('Notification', lazy="joined",
1341 1342 order_by=lambda: Notification.created_on.desc(),)
1342 1343
1343 1344 def mark_as_read(self):
1344 1345 self.read = True
1345 1346 Session.add(self)
1346 1347
1347 1348
1348 1349 class DbMigrateVersion(Base, BaseModel):
1349 1350 __tablename__ = 'db_migrate_version'
1350 1351 __table_args__ = (
1351 1352 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1352 1353 'mysql_charset': 'utf8'},
1353 1354 )
1354 1355 repository_id = Column('repository_id', String(250), primary_key=True)
1355 1356 repository_path = Column('repository_path', Text)
1356 1357 version = Column('version', Integer)
@@ -1,592 +1,589 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import url
30 30 from pylons.i18n.translation import _
31 31
32 32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
33 33 from rhodecode.lib.caching_query import FromCache
34 34
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
37 37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
38 38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup,\
39 39 UsersGroupRepoGroupToPerm
40 40 from rhodecode.lib.exceptions import DefaultUserException, \
41 41 UserOwnsReposException
42 42
43 43 from sqlalchemy.exc import DatabaseError
44 44
45 45 from sqlalchemy.orm import joinedload
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 PERM_WEIGHTS = {
51 51 'repository.none': 0,
52 52 'repository.read': 1,
53 53 'repository.write': 3,
54 54 'repository.admin': 4,
55 55 'group.none': 0,
56 56 'group.read': 1,
57 57 'group.write': 3,
58 58 'group.admin': 4,
59 59 }
60 60
61 61
62 62 class UserModel(BaseModel):
63 63
64 64 def __get_user(self, user):
65 65 return self._get_instance(User, user, callback=User.get_by_username)
66 66
67 67 def __get_perm(self, permission):
68 68 return self._get_instance(Permission, permission,
69 69 callback=Permission.get_by_key)
70 70
71 71 def get(self, user_id, cache=False):
72 72 user = self.sa.query(User)
73 73 if cache:
74 74 user = user.options(FromCache("sql_cache_short",
75 75 "get_user_%s" % user_id))
76 76 return user.get(user_id)
77 77
78 78 def get_user(self, user):
79 79 return self.__get_user(user)
80 80
81 81 def get_by_username(self, username, cache=False, case_insensitive=False):
82 82
83 83 if case_insensitive:
84 84 user = self.sa.query(User).filter(User.username.ilike(username))
85 85 else:
86 86 user = self.sa.query(User)\
87 87 .filter(User.username == username)
88 88 if cache:
89 89 user = user.options(FromCache("sql_cache_short",
90 90 "get_user_%s" % username))
91 91 return user.scalar()
92 92
93 93 def get_by_api_key(self, api_key, cache=False):
94 94 return User.get_by_api_key(api_key, cache)
95 95
96 96 def create(self, form_data):
97 97 try:
98 98 new_user = User()
99 99 for k, v in form_data.items():
100 100 setattr(new_user, k, v)
101 101
102 102 new_user.api_key = generate_api_key(form_data['username'])
103 103 self.sa.add(new_user)
104 104 return new_user
105 105 except:
106 106 log.error(traceback.format_exc())
107 107 raise
108 108
109 109 def create_or_update(self, username, password, email, name, lastname,
110 110 active=True, admin=False, ldap_dn=None):
111 111 """
112 112 Creates a new instance if not found, or updates current one
113 113
114 114 :param username:
115 115 :param password:
116 116 :param email:
117 117 :param active:
118 118 :param name:
119 119 :param lastname:
120 120 :param active:
121 121 :param admin:
122 122 :param ldap_dn:
123 123 """
124 124
125 125 from rhodecode.lib.auth import get_crypt_password
126 126
127 127 log.debug('Checking for %s account in RhodeCode database' % username)
128 128 user = User.get_by_username(username, case_insensitive=True)
129 129 if user is None:
130 130 log.debug('creating new user %s' % username)
131 131 new_user = User()
132 132 else:
133 133 log.debug('updating user %s' % username)
134 134 new_user = user
135 135
136 136 try:
137 137 new_user.username = username
138 138 new_user.admin = admin
139 139 new_user.password = get_crypt_password(password)
140 140 new_user.api_key = generate_api_key(username)
141 141 new_user.email = email
142 142 new_user.active = active
143 143 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
144 144 new_user.name = name
145 145 new_user.lastname = lastname
146 146 self.sa.add(new_user)
147 147 return new_user
148 148 except (DatabaseError,):
149 149 log.error(traceback.format_exc())
150 150 raise
151 151
152 152 def create_for_container_auth(self, username, attrs):
153 153 """
154 154 Creates the given user if it's not already in the database
155 155
156 156 :param username:
157 157 :param attrs:
158 158 """
159 159 if self.get_by_username(username, case_insensitive=True) is None:
160 160
161 161 # autogenerate email for container account without one
162 162 generate_email = lambda usr: '%s@container_auth.account' % usr
163 163
164 164 try:
165 165 new_user = User()
166 166 new_user.username = username
167 167 new_user.password = None
168 168 new_user.api_key = generate_api_key(username)
169 169 new_user.email = attrs['email']
170 170 new_user.active = attrs.get('active', True)
171 171 new_user.name = attrs['name'] or generate_email(username)
172 172 new_user.lastname = attrs['lastname']
173 173
174 174 self.sa.add(new_user)
175 175 return new_user
176 176 except (DatabaseError,):
177 177 log.error(traceback.format_exc())
178 178 self.sa.rollback()
179 179 raise
180 180 log.debug('User %s already exists. Skipping creation of account'
181 181 ' for container auth.', username)
182 182 return None
183 183
184 184 def create_ldap(self, username, password, user_dn, attrs):
185 185 """
186 186 Checks if user is in database, if not creates this user marked
187 187 as ldap user
188 188
189 189 :param username:
190 190 :param password:
191 191 :param user_dn:
192 192 :param attrs:
193 193 """
194 194 from rhodecode.lib.auth import get_crypt_password
195 195 log.debug('Checking for such ldap account in RhodeCode database')
196 196 if self.get_by_username(username, case_insensitive=True) is None:
197 197
198 198 # autogenerate email for ldap account without one
199 199 generate_email = lambda usr: '%s@ldap.account' % usr
200 200
201 201 try:
202 202 new_user = User()
203 203 username = username.lower()
204 204 # add ldap account always lowercase
205 205 new_user.username = username
206 206 new_user.password = get_crypt_password(password)
207 207 new_user.api_key = generate_api_key(username)
208 208 new_user.email = attrs['email'] or generate_email(username)
209 209 new_user.active = attrs.get('active', True)
210 210 new_user.ldap_dn = safe_unicode(user_dn)
211 211 new_user.name = attrs['name']
212 212 new_user.lastname = attrs['lastname']
213 213
214 214 self.sa.add(new_user)
215 215 return new_user
216 216 except (DatabaseError,):
217 217 log.error(traceback.format_exc())
218 218 self.sa.rollback()
219 219 raise
220 220 log.debug('this %s user exists skipping creation of ldap account',
221 221 username)
222 222 return None
223 223
224 224 def create_registration(self, form_data):
225 225 from rhodecode.model.notification import NotificationModel
226 226
227 227 try:
228 new_user = User()
229 for k, v in form_data.items():
230 if k != 'admin':
231 setattr(new_user, k, v)
228 form_data['admin'] = False
229 new_user = self.create(form_data)
232 230
233 231 self.sa.add(new_user)
234 232 self.sa.flush()
235 233
236 234 # notification to admins
237 235 subject = _('new user registration')
238 236 body = ('New user registration\n'
239 237 '---------------------\n'
240 238 '- Username: %s\n'
241 239 '- Full Name: %s\n'
242 240 '- Email: %s\n')
243 241 body = body % (new_user.username, new_user.full_name,
244 242 new_user.email)
245 243 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
246 244 kw = {'registered_user_url': edit_url}
247 245 NotificationModel().create(created_by=new_user, subject=subject,
248 246 body=body, recipients=None,
249 247 type_=Notification.TYPE_REGISTRATION,
250 248 email_kwargs=kw)
251 249
252 250 except:
253 251 log.error(traceback.format_exc())
254 252 raise
255 253
256 254 def update(self, user_id, form_data):
257 255 try:
258 256 user = self.get(user_id, cache=False)
259 257 if user.username == 'default':
260 258 raise DefaultUserException(
261 259 _("You can't Edit this user since it's"
262 260 " crucial for entire application"))
263 261
264 262 for k, v in form_data.items():
265 263 if k == 'new_password' and v != '':
266 264 user.password = v
267 265 user.api_key = generate_api_key(user.username)
268 266 else:
269 267 setattr(user, k, v)
270 268
271 269 self.sa.add(user)
272 270 except:
273 271 log.error(traceback.format_exc())
274 272 raise
275 273
276 274 def update_my_account(self, user_id, form_data):
277 275 try:
278 276 user = self.get(user_id, cache=False)
279 277 if user.username == 'default':
280 278 raise DefaultUserException(
281 279 _("You can't Edit this user since it's"
282 280 " crucial for entire application"))
283 281 for k, v in form_data.items():
284 282 if k == 'new_password' and v != '':
285 283 user.password = v
286 284 user.api_key = generate_api_key(user.username)
287 285 else:
288 286 if k not in ['admin', 'active']:
289 287 setattr(user, k, v)
290 288
291 289 self.sa.add(user)
292 290 except:
293 291 log.error(traceback.format_exc())
294 292 raise
295 293
296 294 def delete(self, user):
297 295 user = self.__get_user(user)
298 296
299 297 try:
300 298 if user.username == 'default':
301 299 raise DefaultUserException(
302 300 _(u"You can't remove this user since it's"
303 301 " crucial for entire application")
304 302 )
305 303 if user.repositories:
306 304 repos = [x.repo_name for x in user.repositories]
307 305 raise UserOwnsReposException(
308 306 _(u'user "%s" still owns %s repositories and cannot be '
309 307 'removed. Switch owners or remove those repositories. %s')
310 308 % (user.username, len(repos), ', '.join(repos))
311 309 )
312 310 self.sa.delete(user)
313 311 except:
314 312 log.error(traceback.format_exc())
315 313 raise
316 314
317 315 def reset_password_link(self, data):
318 316 from rhodecode.lib.celerylib import tasks, run_task
319 317 run_task(tasks.send_password_link, data['email'])
320 318
321 319 def reset_password(self, data):
322 320 from rhodecode.lib.celerylib import tasks, run_task
323 321 run_task(tasks.reset_user_password, data['email'])
324 322
325 323 def fill_data(self, auth_user, user_id=None, api_key=None):
326 324 """
327 325 Fetches auth_user by user_id,or api_key if present.
328 326 Fills auth_user attributes with those taken from database.
329 327 Additionally set's is_authenitated if lookup fails
330 328 present in database
331 329
332 330 :param auth_user: instance of user to set attributes
333 331 :param user_id: user id to fetch by
334 332 :param api_key: api key to fetch by
335 333 """
336 334 if user_id is None and api_key is None:
337 335 raise Exception('You need to pass user_id or api_key')
338 336
339 337 try:
340 338 if api_key:
341 339 dbuser = self.get_by_api_key(api_key)
342 340 else:
343 341 dbuser = self.get(user_id)
344 342
345 343 if dbuser is not None and dbuser.active:
346 344 log.debug('filling %s data' % dbuser)
347 345 for k, v in dbuser.get_dict().items():
348 346 setattr(auth_user, k, v)
349 347 else:
350 348 return False
351 349
352 350 except:
353 351 log.error(traceback.format_exc())
354 352 auth_user.is_authenticated = False
355 353 return False
356 354
357 355 return True
358 356
359 357 def fill_perms(self, user):
360 358 """
361 359 Fills user permission attribute with permissions taken from database
362 360 works for permissions given for repositories, and for permissions that
363 361 are granted to groups
364 362
365 363 :param user: user instance to fill his perms
366 364 """
367 365 RK = 'repositories'
368 366 GK = 'repositories_groups'
369 367 GLOBAL = 'global'
370 368 user.permissions[RK] = {}
371 369 user.permissions[GK] = {}
372 370 user.permissions[GLOBAL] = set()
373 371
374 372 #======================================================================
375 373 # fetch default permissions
376 374 #======================================================================
377 375 default_user = User.get_by_username('default', cache=True)
378 376 default_user_id = default_user.user_id
379 377
380 378 default_repo_perms = Permission.get_default_perms(default_user_id)
381 379 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
382 380
383 381 if user.is_admin:
384 382 #==================================================================
385 383 # admin user have all default rights for repositories
386 384 # and groups set to admin
387 385 #==================================================================
388 386 user.permissions[GLOBAL].add('hg.admin')
389 387
390 388 # repositories
391 389 for perm in default_repo_perms:
392 390 r_k = perm.UserRepoToPerm.repository.repo_name
393 391 p = 'repository.admin'
394 392 user.permissions[RK][r_k] = p
395 393
396 394 # repositories groups
397 395 for perm in default_repo_groups_perms:
398 396 rg_k = perm.UserRepoGroupToPerm.group.group_name
399 397 p = 'group.admin'
400 398 user.permissions[GK][rg_k] = p
401 399 return user
402 400
403 401 #==================================================================
404 402 # set default permissions first for repositories and groups
405 403 #==================================================================
406 404 uid = user.user_id
407 405
408 406 # default global permissions
409 407 default_global_perms = self.sa.query(UserToPerm)\
410 408 .filter(UserToPerm.user_id == default_user_id)
411 409
412 410 for perm in default_global_perms:
413 411 user.permissions[GLOBAL].add(perm.permission.permission_name)
414 412
415 413 # defaults for repositories, taken from default user
416 414 for perm in default_repo_perms:
417 415 r_k = perm.UserRepoToPerm.repository.repo_name
418 416 if perm.Repository.private and not (perm.Repository.user_id == uid):
419 417 # disable defaults for private repos,
420 418 p = 'repository.none'
421 419 elif perm.Repository.user_id == uid:
422 420 # set admin if owner
423 421 p = 'repository.admin'
424 422 else:
425 423 p = perm.Permission.permission_name
426 424
427 425 user.permissions[RK][r_k] = p
428 426
429 427 # defaults for repositories groups taken from default user permission
430 428 # on given group
431 429 for perm in default_repo_groups_perms:
432 430 rg_k = perm.UserRepoGroupToPerm.group.group_name
433 431 p = perm.Permission.permission_name
434 432 user.permissions[GK][rg_k] = p
435 433
436 434 #==================================================================
437 435 # overwrite defaults with user permissions if any found
438 436 #==================================================================
439 437
440 438 # user global permissions
441 439 user_perms = self.sa.query(UserToPerm)\
442 440 .options(joinedload(UserToPerm.permission))\
443 441 .filter(UserToPerm.user_id == uid).all()
444 442
445 443 for perm in user_perms:
446 444 user.permissions[GLOBAL].add(perm.permission.permission_name)
447 445
448 446 # user explicit permissions for repositories
449 447 user_repo_perms = \
450 448 self.sa.query(UserRepoToPerm, Permission, Repository)\
451 449 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
452 450 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
453 451 .filter(UserRepoToPerm.user_id == uid)\
454 452 .all()
455 453
456 454 for perm in user_repo_perms:
457 455 # set admin if owner
458 456 r_k = perm.UserRepoToPerm.repository.repo_name
459 457 if perm.Repository.user_id == uid:
460 458 p = 'repository.admin'
461 459 else:
462 460 p = perm.Permission.permission_name
463 461 user.permissions[RK][r_k] = p
464 462
465 463 # USER GROUP
466 464 #==================================================================
467 465 # check if user is part of user groups for this repository and
468 466 # fill in (or replace with higher) permissions
469 467 #==================================================================
470 468
471 469 # users group global
472 470 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
473 471 .options(joinedload(UsersGroupToPerm.permission))\
474 472 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
475 473 UsersGroupMember.users_group_id))\
476 474 .filter(UsersGroupMember.user_id == uid).all()
477 475
478 476 for perm in user_perms_from_users_groups:
479 477 user.permissions[GLOBAL].add(perm.permission.permission_name)
480 478
481 479 # users group for repositories permissions
482 480 user_repo_perms_from_users_groups = \
483 481 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
484 482 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
485 483 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
486 484 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
487 485 .filter(UsersGroupMember.user_id == uid)\
488 486 .all()
489 487
490 488 for perm in user_repo_perms_from_users_groups:
491 489 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
492 490 p = perm.Permission.permission_name
493 491 cur_perm = user.permissions[RK][r_k]
494 492 # overwrite permission only if it's greater than permission
495 493 # given from other sources
496 494 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
497 495 user.permissions[RK][r_k] = p
498 496
499 497 # REPO GROUP
500 498 #==================================================================
501 499 # get access for this user for repos group and override defaults
502 500 #==================================================================
503 501
504 502 # user explicit permissions for repository
505 503 user_repo_groups_perms = \
506 504 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
507 505 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
508 506 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
509 507 .filter(UserRepoGroupToPerm.user_id == uid)\
510 508 .all()
511 509
512 510 for perm in user_repo_groups_perms:
513 511 rg_k = perm.UserRepoGroupToPerm.group.group_name
514 512 p = perm.Permission.permission_name
515 513 cur_perm = user.permissions[GK][rg_k]
516 514 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
517 515 user.permissions[GK][rg_k] = p
518 516
519 517 # REPO GROUP + USER GROUP
520 518 #==================================================================
521 519 # check if user is part of user groups for this repo group and
522 520 # fill in (or replace with higher) permissions
523 521 #==================================================================
524 522
525 523 # users group for repositories permissions
526 524 user_repo_group_perms_from_users_groups = \
527 525 self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
528 526 .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
529 527 .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
530 528 .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
531 529 .filter(UsersGroupMember.user_id == uid)\
532 530 .all()
533 531
534 532 for perm in user_repo_group_perms_from_users_groups:
535 533 g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
536 print perm, g_k
537 534 p = perm.Permission.permission_name
538 535 cur_perm = user.permissions[GK][g_k]
539 536 # overwrite permission only if it's greater than permission
540 537 # given from other sources
541 538 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
542 539 user.permissions[GK][g_k] = p
543 540
544 541 return user
545 542
546 543 def has_perm(self, user, perm):
547 544 if not isinstance(perm, Permission):
548 545 raise Exception('perm needs to be an instance of Permission class '
549 546 'got %s instead' % type(perm))
550 547
551 548 user = self.__get_user(user)
552 549
553 550 return UserToPerm.query().filter(UserToPerm.user == user)\
554 551 .filter(UserToPerm.permission == perm).scalar() is not None
555 552
556 553 def grant_perm(self, user, perm):
557 554 """
558 555 Grant user global permissions
559 556
560 557 :param user:
561 558 :param perm:
562 559 """
563 560 user = self.__get_user(user)
564 561 perm = self.__get_perm(perm)
565 562 # if this permission is already granted skip it
566 563 _perm = UserToPerm.query()\
567 564 .filter(UserToPerm.user == user)\
568 565 .filter(UserToPerm.permission == perm)\
569 566 .scalar()
570 567 if _perm:
571 568 return
572 569 new = UserToPerm()
573 570 new.user = user
574 571 new.permission = perm
575 572 self.sa.add(new)
576 573
577 574 def revoke_perm(self, user, perm):
578 575 """
579 576 Revoke users global permissions
580 577
581 578 :param user:
582 579 :param perm:
583 580 """
584 581 user = self.__get_user(user)
585 582 perm = self.__get_perm(perm)
586 583
587 584 obj = UserToPerm.query()\
588 585 .filter(UserToPerm.user == user)\
589 586 .filter(UserToPerm.permission == perm)\
590 587 .scalar()
591 588 if obj:
592 589 self.sa.delete(obj)
@@ -1,4422 +1,4426 b''
1 1 html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td
2 2 {
3 3 border: 0;
4 4 outline: 0;
5 5 font-size: 100%;
6 6 vertical-align: baseline;
7 7 background: transparent;
8 8 margin: 0;
9 9 padding: 0;
10 10 }
11 11
12 12 body {
13 13 line-height: 1;
14 14 height: 100%;
15 15 background: url("../images/background.png") repeat scroll 0 0 #B0B0B0;
16 16 font-family: Lucida Grande, Verdana, Lucida Sans Regular,
17 17 Lucida Sans Unicode, Arial, sans-serif; font-size : 12px;
18 18 color: #000;
19 19 margin: 0;
20 20 padding: 0;
21 21 font-size: 12px;
22 22 }
23 23
24 24 ol,ul {
25 25 list-style: none;
26 26 }
27 27
28 28 blockquote,q {
29 29 quotes: none;
30 30 }
31 31
32 32 blockquote:before,blockquote:after,q:before,q:after {
33 33 content: none;
34 34 }
35 35
36 36 :focus {
37 37 outline: 0;
38 38 }
39 39
40 40 del {
41 41 text-decoration: line-through;
42 42 }
43 43
44 44 table {
45 45 border-collapse: collapse;
46 46 border-spacing: 0;
47 47 }
48 48
49 49 html {
50 50 height: 100%;
51 51 }
52 52
53 53 a {
54 54 color: #003367;
55 55 text-decoration: none;
56 56 cursor: pointer;
57 57 }
58 58
59 59 a:hover {
60 60 color: #316293;
61 61 text-decoration: underline;
62 62 }
63 63
64 64 h1,h2,h3,h4,h5,h6 {
65 65 color: #292929;
66 66 font-weight: 700;
67 67 }
68 68
69 69 h1 {
70 70 font-size: 22px;
71 71 }
72 72
73 73 h2 {
74 74 font-size: 20px;
75 75 }
76 76
77 77 h3 {
78 78 font-size: 18px;
79 79 }
80 80
81 81 h4 {
82 82 font-size: 16px;
83 83 }
84 84
85 85 h5 {
86 86 font-size: 14px;
87 87 }
88 88
89 89 h6 {
90 90 font-size: 11px;
91 91 }
92 92
93 93 ul.circle {
94 94 list-style-type: circle;
95 95 }
96 96
97 97 ul.disc {
98 98 list-style-type: disc;
99 99 }
100 100
101 101 ul.square {
102 102 list-style-type: square;
103 103 }
104 104
105 105 ol.lower-roman {
106 106 list-style-type: lower-roman;
107 107 }
108 108
109 109 ol.upper-roman {
110 110 list-style-type: upper-roman;
111 111 }
112 112
113 113 ol.lower-alpha {
114 114 list-style-type: lower-alpha;
115 115 }
116 116
117 117 ol.upper-alpha {
118 118 list-style-type: upper-alpha;
119 119 }
120 120
121 121 ol.decimal {
122 122 list-style-type: decimal;
123 123 }
124 124
125 125 div.color {
126 126 clear: both;
127 127 overflow: hidden;
128 128 position: absolute;
129 129 background: #FFF;
130 130 margin: 7px 0 0 60px;
131 131 padding: 1px 1px 1px 0;
132 132 }
133 133
134 134 div.color a {
135 135 width: 15px;
136 136 height: 15px;
137 137 display: block;
138 138 float: left;
139 139 margin: 0 0 0 1px;
140 140 padding: 0;
141 141 }
142 142
143 143 div.options {
144 144 clear: both;
145 145 overflow: hidden;
146 146 position: absolute;
147 147 background: #FFF;
148 148 margin: 7px 0 0 162px;
149 149 padding: 0;
150 150 }
151 151
152 152 div.options a {
153 153 height: 1%;
154 154 display: block;
155 155 text-decoration: none;
156 156 margin: 0;
157 157 padding: 3px 8px;
158 158 }
159 159
160 160 .top-left-rounded-corner {
161 161 -webkit-border-top-left-radius: 8px;
162 162 -khtml-border-radius-topleft: 8px;
163 163 -moz-border-radius-topleft: 8px;
164 164 border-top-left-radius: 8px;
165 165 }
166 166
167 167 .top-right-rounded-corner {
168 168 -webkit-border-top-right-radius: 8px;
169 169 -khtml-border-radius-topright: 8px;
170 170 -moz-border-radius-topright: 8px;
171 171 border-top-right-radius: 8px;
172 172 }
173 173
174 174 .bottom-left-rounded-corner {
175 175 -webkit-border-bottom-left-radius: 8px;
176 176 -khtml-border-radius-bottomleft: 8px;
177 177 -moz-border-radius-bottomleft: 8px;
178 178 border-bottom-left-radius: 8px;
179 179 }
180 180
181 181 .bottom-right-rounded-corner {
182 182 -webkit-border-bottom-right-radius: 8px;
183 183 -khtml-border-radius-bottomright: 8px;
184 184 -moz-border-radius-bottomright: 8px;
185 185 border-bottom-right-radius: 8px;
186 186 }
187 187
188 188 .top-left-rounded-corner-mid {
189 189 -webkit-border-top-left-radius: 4px;
190 190 -khtml-border-radius-topleft: 4px;
191 191 -moz-border-radius-topleft: 4px;
192 192 border-top-left-radius: 4px;
193 193 }
194 194
195 195 .top-right-rounded-corner-mid {
196 196 -webkit-border-top-right-radius: 4px;
197 197 -khtml-border-radius-topright: 4px;
198 198 -moz-border-radius-topright: 4px;
199 199 border-top-right-radius: 4px;
200 200 }
201 201
202 202 .bottom-left-rounded-corner-mid {
203 203 -webkit-border-bottom-left-radius: 4px;
204 204 -khtml-border-radius-bottomleft: 4px;
205 205 -moz-border-radius-bottomleft: 4px;
206 206 border-bottom-left-radius: 4px;
207 207 }
208 208
209 209 .bottom-right-rounded-corner-mid {
210 210 -webkit-border-bottom-right-radius: 4px;
211 211 -khtml-border-radius-bottomright: 4px;
212 212 -moz-border-radius-bottomright: 4px;
213 213 border-bottom-right-radius: 4px;
214 214 }
215 215
216 216 .help-block {
217 217 color: #999999;
218 218 display: block;
219 219 margin-bottom: 0;
220 220 margin-top: 5px;
221 221 }
222 222
223 223 #header {
224 224 margin: 0;
225 225 padding: 0 10px;
226 226 }
227 227
228 228 #header ul#logged-user {
229 229 margin-bottom: 5px !important;
230 230 -webkit-border-radius: 0px 0px 8px 8px;
231 231 -khtml-border-radius: 0px 0px 8px 8px;
232 232 -moz-border-radius: 0px 0px 8px 8px;
233 233 border-radius: 0px 0px 8px 8px;
234 234 height: 37px;
235 235 background-color: #003B76;
236 236 background-repeat: repeat-x;
237 237 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
238 238 background-image: -moz-linear-gradient(top, #003b76, #00376e);
239 239 background-image: -ms-linear-gradient(top, #003b76, #00376e);
240 240 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
241 241 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
242 242 background-image: -o-linear-gradient(top, #003b76, #00376e);
243 243 background-image: linear-gradient(top, #003b76, #00376e);
244 244 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
245 245 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
246 246 }
247 247
248 248 #header ul#logged-user li {
249 249 list-style: none;
250 250 float: left;
251 251 margin: 8px 0 0;
252 252 padding: 4px 12px;
253 253 border-left: 1px solid #316293;
254 254 }
255 255
256 256 #header ul#logged-user li.first {
257 257 border-left: none;
258 258 margin: 4px;
259 259 }
260 260
261 261 #header ul#logged-user li.first div.gravatar {
262 262 margin-top: -2px;
263 263 }
264 264
265 265 #header ul#logged-user li.first div.account {
266 266 padding-top: 4px;
267 267 float: left;
268 268 }
269 269
270 270 #header ul#logged-user li.last {
271 271 border-right: none;
272 272 }
273 273
274 274 #header ul#logged-user li a {
275 275 color: #fff;
276 276 font-weight: 700;
277 277 text-decoration: none;
278 278 }
279 279
280 280 #header ul#logged-user li a:hover {
281 281 text-decoration: underline;
282 282 }
283 283
284 284 #header ul#logged-user li.highlight a {
285 285 color: #fff;
286 286 }
287 287
288 288 #header ul#logged-user li.highlight a:hover {
289 289 color: #FFF;
290 290 }
291 291
292 292 #header #header-inner {
293 293 min-height: 44px;
294 294 clear: both;
295 295 position: relative;
296 296 background-color: #003B76;
297 297 background-repeat: repeat-x;
298 298 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
299 299 background-image: -moz-linear-gradient(top, #003b76, #00376e);
300 300 background-image: -ms-linear-gradient(top, #003b76, #00376e);
301 301 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76),color-stop(100%, #00376e) );
302 302 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
303 303 background-image: -o-linear-gradient(top, #003b76, #00376e);
304 304 background-image: linear-gradient(top, #003b76, #00376e);
305 305 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
306 306 margin: 0;
307 307 padding: 0;
308 308 display: block;
309 309 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
310 310 -webkit-border-radius: 4px 4px 4px 4px;
311 311 -khtml-border-radius: 4px 4px 4px 4px;
312 312 -moz-border-radius: 4px 4px 4px 4px;
313 313 border-radius: 4px 4px 4px 4px;
314 314 }
315 315 #header #header-inner.hover{
316 316 position: fixed !important;
317 317 width: 100% !important;
318 318 margin-left: -10px !important;
319 319 z-index: 10000;
320 320 -webkit-border-radius: 0px 0px 0px 0px;
321 321 -khtml-border-radius: 0px 0px 0px 0px;
322 322 -moz-border-radius: 0px 0px 0px 0px;
323 323 border-radius: 0px 0px 0px 0px;
324 324 }
325 325
326 326 .ie7 #header #header-inner.hover,
327 327 .ie8 #header #header-inner.hover,
328 328 .ie9 #header #header-inner.hover
329 329 {
330 330 z-index: auto !important;
331 331 }
332 332
333 333 #header #header-inner #home a {
334 334 height: 40px;
335 335 width: 46px;
336 336 display: block;
337 337 background: url("../images/button_home.png");
338 338 background-position: 0 0;
339 339 margin: 0;
340 340 padding: 0;
341 341 }
342 342
343 343 #header #header-inner #home a:hover {
344 344 background-position: 0 -40px;
345 345 }
346 346
347 347 #header #header-inner #logo {
348 348 float: left;
349 349 position: absolute;
350 350 }
351 351
352 352 #header #header-inner #logo h1 {
353 353 color: #FFF;
354 354 font-size: 20px;
355 355 margin: 12px 0 0 13px;
356 356 padding: 0;
357 357 }
358 358
359 359 #header #header-inner #logo a {
360 360 color: #fff;
361 361 text-decoration: none;
362 362 }
363 363
364 364 #header #header-inner #logo a:hover {
365 365 color: #bfe3ff;
366 366 }
367 367
368 368 #header #header-inner #quick,#header #header-inner #quick ul {
369 369 position: relative;
370 370 float: right;
371 371 list-style-type: none;
372 372 list-style-position: outside;
373 373 margin: 8px 8px 0 0;
374 374 padding: 0;
375 375 }
376 376
377 377 #header #header-inner #quick li {
378 378 position: relative;
379 379 float: left;
380 380 margin: 0 5px 0 0;
381 381 padding: 0;
382 382 }
383 383
384 384 #header #header-inner #quick li a.menu_link {
385 385 top: 0;
386 386 left: 0;
387 387 height: 1%;
388 388 display: block;
389 389 clear: both;
390 390 overflow: hidden;
391 391 color: #FFF;
392 392 font-weight: 700;
393 393 text-decoration: none;
394 394 background: #369;
395 395 padding: 0;
396 396 -webkit-border-radius: 4px 4px 4px 4px;
397 397 -khtml-border-radius: 4px 4px 4px 4px;
398 398 -moz-border-radius: 4px 4px 4px 4px;
399 399 border-radius: 4px 4px 4px 4px;
400 400 }
401 401
402 402 #header #header-inner #quick li span.short {
403 403 padding: 9px 6px 8px 6px;
404 404 }
405 405
406 406 #header #header-inner #quick li span {
407 407 top: 0;
408 408 right: 0;
409 409 height: 1%;
410 410 display: block;
411 411 float: left;
412 412 border-left: 1px solid #3f6f9f;
413 413 margin: 0;
414 414 padding: 10px 12px 8px 10px;
415 415 }
416 416
417 417 #header #header-inner #quick li span.normal {
418 418 border: none;
419 419 padding: 10px 12px 8px;
420 420 }
421 421
422 422 #header #header-inner #quick li span.icon {
423 423 top: 0;
424 424 left: 0;
425 425 border-left: none;
426 426 border-right: 1px solid #2e5c89;
427 427 padding: 8px 6px 4px;
428 428 }
429 429
430 430 #header #header-inner #quick li span.icon_short {
431 431 top: 0;
432 432 left: 0;
433 433 border-left: none;
434 434 border-right: 1px solid #2e5c89;
435 435 padding: 8px 6px 4px;
436 436 }
437 437
438 438 #header #header-inner #quick li span.icon img,#header #header-inner #quick li span.icon_short img
439 439 {
440 440 margin: 0px -2px 0px 0px;
441 441 }
442 442
443 443 #header #header-inner #quick li a:hover {
444 444 background: #4e4e4e no-repeat top left;
445 445 }
446 446
447 447 #header #header-inner #quick li a:hover span {
448 448 border-left: 1px solid #545454;
449 449 }
450 450
451 451 #header #header-inner #quick li a:hover span.icon,#header #header-inner #quick li a:hover span.icon_short
452 452 {
453 453 border-left: none;
454 454 border-right: 1px solid #464646;
455 455 }
456 456
457 457 #header #header-inner #quick ul {
458 458 top: 29px;
459 459 right: 0;
460 460 min-width: 200px;
461 461 display: none;
462 462 position: absolute;
463 463 background: #FFF;
464 464 border: 1px solid #666;
465 465 border-top: 1px solid #003367;
466 466 z-index: 100;
467 467 margin: 0px 0px 0px 0px;
468 468 padding: 0;
469 469 }
470 470
471 471 #header #header-inner #quick ul.repo_switcher {
472 472 max-height: 275px;
473 473 overflow-x: hidden;
474 474 overflow-y: auto;
475 475 }
476 476
477 477 #header #header-inner #quick ul.repo_switcher li.qfilter_rs {
478 478 float: none;
479 479 margin: 0;
480 480 border-bottom: 2px solid #003367;
481 481 }
482 482
483 483 #header #header-inner #quick .repo_switcher_type {
484 484 position: absolute;
485 485 left: 0;
486 486 top: 9px;
487 487 }
488 488
489 489 #header #header-inner #quick li ul li {
490 490 border-bottom: 1px solid #ddd;
491 491 }
492 492
493 493 #header #header-inner #quick li ul li a {
494 494 width: 182px;
495 495 height: auto;
496 496 display: block;
497 497 float: left;
498 498 background: #FFF;
499 499 color: #003367;
500 500 font-weight: 400;
501 501 margin: 0;
502 502 padding: 7px 9px;
503 503 }
504 504
505 505 #header #header-inner #quick li ul li a:hover {
506 506 color: #000;
507 507 background: #FFF;
508 508 }
509 509
510 510 #header #header-inner #quick ul ul {
511 511 top: auto;
512 512 }
513 513
514 514 #header #header-inner #quick li ul ul {
515 515 right: 200px;
516 516 max-height: 275px;
517 517 overflow: auto;
518 518 overflow-x: hidden;
519 519 white-space: normal;
520 520 }
521 521
522 522 #header #header-inner #quick li ul li a.journal,#header #header-inner #quick li ul li a.journal:hover
523 523 {
524 524 background: url("../images/icons/book.png") no-repeat scroll 4px 9px
525 525 #FFF;
526 526 width: 167px;
527 527 margin: 0;
528 528 padding: 12px 9px 7px 24px;
529 529 }
530 530
531 531 #header #header-inner #quick li ul li a.private_repo,#header #header-inner #quick li ul li a.private_repo:hover
532 532 {
533 533 background: url("../images/icons/lock.png") no-repeat scroll 4px 9px
534 534 #FFF;
535 535 min-width: 167px;
536 536 margin: 0;
537 537 padding: 12px 9px 7px 24px;
538 538 }
539 539
540 540 #header #header-inner #quick li ul li a.public_repo,#header #header-inner #quick li ul li a.public_repo:hover
541 541 {
542 542 background: url("../images/icons/lock_open.png") no-repeat scroll 4px
543 543 9px #FFF;
544 544 min-width: 167px;
545 545 margin: 0;
546 546 padding: 12px 9px 7px 24px;
547 547 }
548 548
549 549 #header #header-inner #quick li ul li a.hg,#header #header-inner #quick li ul li a.hg:hover
550 550 {
551 551 background: url("../images/icons/hgicon.png") no-repeat scroll 4px 9px
552 552 #FFF;
553 553 min-width: 167px;
554 554 margin: 0 0 0 14px;
555 555 padding: 12px 9px 7px 24px;
556 556 }
557 557
558 558 #header #header-inner #quick li ul li a.git,#header #header-inner #quick li ul li a.git:hover
559 559 {
560 560 background: url("../images/icons/giticon.png") no-repeat scroll 4px 9px
561 561 #FFF;
562 562 min-width: 167px;
563 563 margin: 0 0 0 14px;
564 564 padding: 12px 9px 7px 24px;
565 565 }
566 566
567 567 #header #header-inner #quick li ul li a.repos,#header #header-inner #quick li ul li a.repos:hover
568 568 {
569 569 background: url("../images/icons/database_edit.png") no-repeat scroll
570 570 4px 9px #FFF;
571 571 width: 167px;
572 572 margin: 0;
573 573 padding: 12px 9px 7px 24px;
574 574 }
575 575
576 576 #header #header-inner #quick li ul li a.repos_groups,#header #header-inner #quick li ul li a.repos_groups:hover
577 577 {
578 578 background: url("../images/icons/database_link.png") no-repeat scroll
579 579 4px 9px #FFF;
580 580 width: 167px;
581 581 margin: 0;
582 582 padding: 12px 9px 7px 24px;
583 583 }
584 584
585 585 #header #header-inner #quick li ul li a.users,#header #header-inner #quick li ul li a.users:hover
586 586 {
587 587 background: #FFF url("../images/icons/user_edit.png") no-repeat 4px 9px;
588 588 width: 167px;
589 589 margin: 0;
590 590 padding: 12px 9px 7px 24px;
591 591 }
592 592
593 593 #header #header-inner #quick li ul li a.groups,#header #header-inner #quick li ul li a.groups:hover
594 594 {
595 595 background: #FFF url("../images/icons/group_edit.png") no-repeat 4px 9px;
596 596 width: 167px;
597 597 margin: 0;
598 598 padding: 12px 9px 7px 24px;
599 599 }
600 600
601 601 #header #header-inner #quick li ul li a.settings,#header #header-inner #quick li ul li a.settings:hover
602 602 {
603 603 background: #FFF url("../images/icons/cog.png") no-repeat 4px 9px;
604 604 width: 167px;
605 605 margin: 0;
606 606 padding: 12px 9px 7px 24px;
607 607 }
608 608
609 609 #header #header-inner #quick li ul li a.permissions,#header #header-inner #quick li ul li a.permissions:hover
610 610 {
611 611 background: #FFF url("../images/icons/key.png") no-repeat 4px 9px;
612 612 width: 167px;
613 613 margin: 0;
614 614 padding: 12px 9px 7px 24px;
615 615 }
616 616
617 617 #header #header-inner #quick li ul li a.ldap,#header #header-inner #quick li ul li a.ldap:hover
618 618 {
619 619 background: #FFF url("../images/icons/server_key.png") no-repeat 4px 9px;
620 620 width: 167px;
621 621 margin: 0;
622 622 padding: 12px 9px 7px 24px;
623 623 }
624 624
625 625 #header #header-inner #quick li ul li a.fork,#header #header-inner #quick li ul li a.fork:hover
626 626 {
627 627 background: #FFF url("../images/icons/arrow_divide.png") no-repeat 4px
628 628 9px;
629 629 width: 167px;
630 630 margin: 0;
631 631 padding: 12px 9px 7px 24px;
632 632 }
633 633
634 634 #header #header-inner #quick li ul li a.search,#header #header-inner #quick li ul li a.search:hover
635 635 {
636 636 background: #FFF url("../images/icons/search_16.png") no-repeat 4px 9px;
637 637 width: 167px;
638 638 margin: 0;
639 639 padding: 12px 9px 7px 24px;
640 640 }
641 641
642 642 #header #header-inner #quick li ul li a.delete,#header #header-inner #quick li ul li a.delete:hover
643 643 {
644 644 background: #FFF url("../images/icons/delete.png") no-repeat 4px 9px;
645 645 width: 167px;
646 646 margin: 0;
647 647 padding: 12px 9px 7px 24px;
648 648 }
649 649
650 650 #header #header-inner #quick li ul li a.branches,#header #header-inner #quick li ul li a.branches:hover
651 651 {
652 652 background: #FFF url("../images/icons/arrow_branch.png") no-repeat 4px
653 653 9px;
654 654 width: 167px;
655 655 margin: 0;
656 656 padding: 12px 9px 7px 24px;
657 657 }
658 658
659 659 #header #header-inner #quick li ul li a.tags,
660 660 #header #header-inner #quick li ul li a.tags:hover{
661 661 background: #FFF url("../images/icons/tag_blue.png") no-repeat 4px 9px;
662 662 width: 167px;
663 663 margin: 0;
664 664 padding: 12px 9px 7px 24px;
665 665 }
666 666
667 667 #header #header-inner #quick li ul li a.bookmarks,
668 668 #header #header-inner #quick li ul li a.bookmarks:hover{
669 669 background: #FFF url("../images/icons/tag_green.png") no-repeat 4px 9px;
670 670 width: 167px;
671 671 margin: 0;
672 672 padding: 12px 9px 7px 24px;
673 673 }
674 674
675 675 #header #header-inner #quick li ul li a.admin,
676 676 #header #header-inner #quick li ul li a.admin:hover{
677 677 background: #FFF url("../images/icons/cog_edit.png") no-repeat 4px 9px;
678 678 width: 167px;
679 679 margin: 0;
680 680 padding: 12px 9px 7px 24px;
681 681 }
682 682
683 683 .groups_breadcrumbs a {
684 684 color: #fff;
685 685 }
686 686
687 687 .groups_breadcrumbs a:hover {
688 688 color: #bfe3ff;
689 689 text-decoration: none;
690 690 }
691 691
692 692 td.quick_repo_menu {
693 693 background: #FFF url("../images/vertical-indicator.png") 8px 50% no-repeat !important;
694 694 cursor: pointer;
695 695 width: 8px;
696 696 border: 1px solid transparent;
697 697 }
698 698
699 699 td.quick_repo_menu.active {
700 700 background: url("../images/dt-arrow-dn.png") no-repeat scroll 5px 50% #FFFFFF !important;
701 701 border: 1px solid #003367;
702 702 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
703 703 cursor: pointer;
704 704 }
705 705
706 706 td.quick_repo_menu .menu_items {
707 707 margin-top: 10px;
708 708 margin-left:-6px;
709 709 width: 150px;
710 710 position: absolute;
711 711 background-color: #FFF;
712 712 background: none repeat scroll 0 0 #FFFFFF;
713 713 border-color: #003367 #666666 #666666;
714 714 border-right: 1px solid #666666;
715 715 border-style: solid;
716 716 border-width: 1px;
717 717 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
718 718 border-top-style: none;
719 719 }
720 720
721 721 td.quick_repo_menu .menu_items li {
722 722 padding: 0 !important;
723 723 }
724 724
725 725 td.quick_repo_menu .menu_items a {
726 726 display: block;
727 727 padding: 4px 12px 4px 8px;
728 728 }
729 729
730 730 td.quick_repo_menu .menu_items a:hover {
731 731 background-color: #EEE;
732 732 text-decoration: none;
733 733 }
734 734
735 735 td.quick_repo_menu .menu_items .icon img {
736 736 margin-bottom: -2px;
737 737 }
738 738
739 739 td.quick_repo_menu .menu_items.hidden {
740 740 display: none;
741 741 }
742 742
743 743 .yui-dt-first th {
744 744 text-align: left;
745 745 }
746 746
747 747 /*
748 748 Copyright (c) 2011, Yahoo! Inc. All rights reserved.
749 749 Code licensed under the BSD License:
750 750 http://developer.yahoo.com/yui/license.html
751 751 version: 2.9.0
752 752 */
753 753 .yui-skin-sam .yui-dt-mask {
754 754 position: absolute;
755 755 z-index: 9500;
756 756 }
757 757 .yui-dt-tmp {
758 758 position: absolute;
759 759 left: -9000px;
760 760 }
761 761 .yui-dt-scrollable .yui-dt-bd { overflow: auto }
762 762 .yui-dt-scrollable .yui-dt-hd {
763 763 overflow: hidden;
764 764 position: relative;
765 765 }
766 766 .yui-dt-scrollable .yui-dt-bd thead tr,
767 767 .yui-dt-scrollable .yui-dt-bd thead th {
768 768 position: absolute;
769 769 left: -1500px;
770 770 }
771 771 .yui-dt-scrollable tbody { -moz-outline: 0 }
772 772 .yui-skin-sam thead .yui-dt-sortable { cursor: pointer }
773 773 .yui-skin-sam thead .yui-dt-draggable { cursor: move }
774 774 .yui-dt-coltarget {
775 775 position: absolute;
776 776 z-index: 999;
777 777 }
778 778 .yui-dt-hd { zoom: 1 }
779 779 th.yui-dt-resizeable .yui-dt-resizerliner { position: relative }
780 780 .yui-dt-resizer {
781 781 position: absolute;
782 782 right: 0;
783 783 bottom: 0;
784 784 height: 100%;
785 785 cursor: e-resize;
786 786 cursor: col-resize;
787 787 background-color: #CCC;
788 788 opacity: 0;
789 789 filter: alpha(opacity=0);
790 790 }
791 791 .yui-dt-resizerproxy {
792 792 visibility: hidden;
793 793 position: absolute;
794 794 z-index: 9000;
795 795 background-color: #CCC;
796 796 opacity: 0;
797 797 filter: alpha(opacity=0);
798 798 }
799 799 th.yui-dt-hidden .yui-dt-liner,
800 800 td.yui-dt-hidden .yui-dt-liner,
801 801 th.yui-dt-hidden .yui-dt-resizer { display: none }
802 802 .yui-dt-editor,
803 803 .yui-dt-editor-shim {
804 804 position: absolute;
805 805 z-index: 9000;
806 806 }
807 807 .yui-skin-sam .yui-dt table {
808 808 margin: 0;
809 809 padding: 0;
810 810 font-family: arial;
811 811 font-size: inherit;
812 812 border-collapse: separate;
813 813 *border-collapse: collapse;
814 814 border-spacing: 0;
815 815 border: 1px solid #7f7f7f;
816 816 }
817 817 .yui-skin-sam .yui-dt thead { border-spacing: 0 }
818 818 .yui-skin-sam .yui-dt caption {
819 819 color: #000;
820 820 font-size: 85%;
821 821 font-weight: normal;
822 822 font-style: italic;
823 823 line-height: 1;
824 824 padding: 1em 0;
825 825 text-align: center;
826 826 }
827 827 .yui-skin-sam .yui-dt th { background: #d8d8da url(../images/sprite.png) repeat-x 0 0 }
828 828 .yui-skin-sam .yui-dt th,
829 829 .yui-skin-sam .yui-dt th a {
830 830 font-weight: normal;
831 831 text-decoration: none;
832 832 color: #000;
833 833 vertical-align: bottom;
834 834 }
835 835 .yui-skin-sam .yui-dt th {
836 836 margin: 0;
837 837 padding: 0;
838 838 border: 0;
839 839 border-right: 1px solid #cbcbcb;
840 840 }
841 841 .yui-skin-sam .yui-dt tr.yui-dt-first td { border-top: 1px solid #7f7f7f }
842 842 .yui-skin-sam .yui-dt th .yui-dt-liner { white-space: nowrap }
843 843 .yui-skin-sam .yui-dt-liner {
844 844 margin: 0;
845 845 padding: 0;
846 846 }
847 847 .yui-skin-sam .yui-dt-coltarget {
848 848 width: 5px;
849 849 background-color: red;
850 850 }
851 851 .yui-skin-sam .yui-dt td {
852 852 margin: 0;
853 853 padding: 0;
854 854 border: 0;
855 855 border-right: 1px solid #cbcbcb;
856 856 text-align: left;
857 857 }
858 858 .yui-skin-sam .yui-dt-list td { border-right: 0 }
859 859 .yui-skin-sam .yui-dt-resizer { width: 6px }
860 860 .yui-skin-sam .yui-dt-mask {
861 861 background-color: #000;
862 862 opacity: .25;
863 863 filter: alpha(opacity=25);
864 864 }
865 865 .yui-skin-sam .yui-dt-message { background-color: #FFF }
866 866 .yui-skin-sam .yui-dt-scrollable table { border: 0 }
867 867 .yui-skin-sam .yui-dt-scrollable .yui-dt-hd {
868 868 border-left: 1px solid #7f7f7f;
869 869 border-top: 1px solid #7f7f7f;
870 870 border-right: 1px solid #7f7f7f;
871 871 }
872 872 .yui-skin-sam .yui-dt-scrollable .yui-dt-bd {
873 873 border-left: 1px solid #7f7f7f;
874 874 border-bottom: 1px solid #7f7f7f;
875 875 border-right: 1px solid #7f7f7f;
876 876 background-color: #FFF;
877 877 }
878 878 .yui-skin-sam .yui-dt-scrollable .yui-dt-data tr.yui-dt-last td { border-bottom: 1px solid #7f7f7f }
879 879 .yui-skin-sam th.yui-dt-asc,
880 880 .yui-skin-sam th.yui-dt-desc { background: url(../images/sprite.png) repeat-x 0 -100px }
881 881 .yui-skin-sam th.yui-dt-sortable .yui-dt-label { margin-right: 10px }
882 882 .yui-skin-sam th.yui-dt-asc .yui-dt-liner { background: url(../images/dt-arrow-up.png) no-repeat right }
883 883 .yui-skin-sam th.yui-dt-desc .yui-dt-liner { background: url(../images/dt-arrow-dn.png) no-repeat right }
884 884 tbody .yui-dt-editable { cursor: pointer }
885 885 .yui-dt-editor {
886 886 text-align: left;
887 887 background-color: #f2f2f2;
888 888 border: 1px solid #808080;
889 889 padding: 6px;
890 890 }
891 891 .yui-dt-editor label {
892 892 padding-left: 4px;
893 893 padding-right: 6px;
894 894 }
895 895 .yui-dt-editor .yui-dt-button {
896 896 padding-top: 6px;
897 897 text-align: right;
898 898 }
899 899 .yui-dt-editor .yui-dt-button button {
900 900 background: url(../images/sprite.png) repeat-x 0 0;
901 901 border: 1px solid #999;
902 902 width: 4em;
903 903 height: 1.8em;
904 904 margin-left: 6px;
905 905 }
906 906 .yui-dt-editor .yui-dt-button button.yui-dt-default {
907 907 background: url(../images/sprite.png) repeat-x 0 -1400px;
908 908 background-color: #5584e0;
909 909 border: 1px solid #304369;
910 910 color: #FFF;
911 911 }
912 912 .yui-dt-editor .yui-dt-button button:hover {
913 913 background: url(../images/sprite.png) repeat-x 0 -1300px;
914 914 color: #000;
915 915 }
916 916 .yui-dt-editor .yui-dt-button button:active {
917 917 background: url(../images/sprite.png) repeat-x 0 -1700px;
918 918 color: #000;
919 919 }
920 920 .yui-skin-sam tr.yui-dt-even { background-color: #FFF }
921 921 .yui-skin-sam tr.yui-dt-odd { background-color: #edf5ff }
922 922 .yui-skin-sam tr.yui-dt-even td.yui-dt-asc,
923 923 .yui-skin-sam tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
924 924 .yui-skin-sam tr.yui-dt-odd td.yui-dt-asc,
925 925 .yui-skin-sam tr.yui-dt-odd td.yui-dt-desc { background-color: #dbeaff }
926 926 .yui-skin-sam .yui-dt-list tr.yui-dt-even { background-color: #FFF }
927 927 .yui-skin-sam .yui-dt-list tr.yui-dt-odd { background-color: #FFF }
928 928 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-asc,
929 929 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
930 930 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-asc,
931 931 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-desc { background-color: #edf5ff }
932 932 .yui-skin-sam th.yui-dt-highlighted,
933 933 .yui-skin-sam th.yui-dt-highlighted a { background-color: #b2d2ff }
934 934 .yui-skin-sam tr.yui-dt-highlighted,
935 935 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-asc,
936 936 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-desc,
937 937 .yui-skin-sam tr.yui-dt-even td.yui-dt-highlighted,
938 938 .yui-skin-sam tr.yui-dt-odd td.yui-dt-highlighted {
939 939 cursor: pointer;
940 940 background-color: #b2d2ff;
941 941 }
942 942 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted,
943 943 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted a { background-color: #b2d2ff }
944 944 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted,
945 945 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-asc,
946 946 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-desc,
947 947 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-highlighted,
948 948 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-highlighted {
949 949 cursor: pointer;
950 950 background-color: #b2d2ff;
951 951 }
952 952 .yui-skin-sam th.yui-dt-selected,
953 953 .yui-skin-sam th.yui-dt-selected a { background-color: #446cd7 }
954 954 .yui-skin-sam tr.yui-dt-selected td,
955 955 .yui-skin-sam tr.yui-dt-selected td.yui-dt-asc,
956 956 .yui-skin-sam tr.yui-dt-selected td.yui-dt-desc {
957 957 background-color: #426fd9;
958 958 color: #FFF;
959 959 }
960 960 .yui-skin-sam tr.yui-dt-even td.yui-dt-selected,
961 961 .yui-skin-sam tr.yui-dt-odd td.yui-dt-selected {
962 962 background-color: #446cd7;
963 963 color: #FFF;
964 964 }
965 965 .yui-skin-sam .yui-dt-list th.yui-dt-selected,
966 966 .yui-skin-sam .yui-dt-list th.yui-dt-selected a { background-color: #446cd7 }
967 967 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td,
968 968 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-asc,
969 969 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-desc {
970 970 background-color: #426fd9;
971 971 color: #FFF;
972 972 }
973 973 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-selected,
974 974 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-selected {
975 975 background-color: #446cd7;
976 976 color: #FFF;
977 977 }
978 978 .yui-skin-sam .yui-dt-paginator {
979 979 display: block;
980 980 margin: 6px 0;
981 981 white-space: nowrap;
982 982 }
983 983 .yui-skin-sam .yui-dt-paginator .yui-dt-first,
984 984 .yui-skin-sam .yui-dt-paginator .yui-dt-last,
985 985 .yui-skin-sam .yui-dt-paginator .yui-dt-selected { padding: 2px 6px }
986 986 .yui-skin-sam .yui-dt-paginator a.yui-dt-first,
987 987 .yui-skin-sam .yui-dt-paginator a.yui-dt-last { text-decoration: none }
988 988 .yui-skin-sam .yui-dt-paginator .yui-dt-previous,
989 989 .yui-skin-sam .yui-dt-paginator .yui-dt-next { display: none }
990 990 .yui-skin-sam a.yui-dt-page {
991 991 border: 1px solid #cbcbcb;
992 992 padding: 2px 6px;
993 993 text-decoration: none;
994 994 background-color: #fff;
995 995 }
996 996 .yui-skin-sam .yui-dt-selected {
997 997 border: 1px solid #fff;
998 998 background-color: #fff;
999 999 }
1000 1000
1001 1001 #content #left {
1002 1002 left: 0;
1003 1003 width: 280px;
1004 1004 position: absolute;
1005 1005 }
1006 1006
1007 1007 #content #right {
1008 1008 margin: 0 60px 10px 290px;
1009 1009 }
1010 1010
1011 1011 #content div.box {
1012 1012 clear: both;
1013 1013 overflow: hidden;
1014 1014 background: #fff;
1015 1015 margin: 0 0 10px;
1016 1016 padding: 0 0 10px;
1017 1017 -webkit-border-radius: 4px 4px 4px 4px;
1018 1018 -khtml-border-radius: 4px 4px 4px 4px;
1019 1019 -moz-border-radius: 4px 4px 4px 4px;
1020 1020 border-radius: 4px 4px 4px 4px;
1021 1021 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1022 1022 }
1023 1023
1024 1024 #content div.box-left {
1025 1025 width: 49%;
1026 1026 clear: none;
1027 1027 float: left;
1028 1028 margin: 0 0 10px;
1029 1029 }
1030 1030
1031 1031 #content div.box-right {
1032 1032 width: 49%;
1033 1033 clear: none;
1034 1034 float: right;
1035 1035 margin: 0 0 10px;
1036 1036 }
1037 1037
1038 1038 #content div.box div.title {
1039 1039 clear: both;
1040 1040 overflow: hidden;
1041 1041 background-color: #003B76;
1042 1042 background-repeat: repeat-x;
1043 1043 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
1044 1044 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1045 1045 background-image: -ms-linear-gradient(top, #003b76, #00376e);
1046 1046 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
1047 1047 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
1048 1048 background-image: -o-linear-gradient(top, #003b76, #00376e);
1049 1049 background-image: linear-gradient(top, #003b76, #00376e);
1050 1050 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
1051 1051 margin: 0 0 20px;
1052 1052 padding: 0;
1053 1053 }
1054 1054
1055 1055 #content div.box div.title h5 {
1056 1056 float: left;
1057 1057 border: none;
1058 1058 color: #fff;
1059 1059 text-transform: uppercase;
1060 1060 margin: 0;
1061 1061 padding: 11px 0 11px 10px;
1062 1062 }
1063 1063
1064 1064 #content div.box div.title .link-white{
1065 1065 color: #FFFFFF;
1066 1066 }
1067 1067
1068 1068 #content div.box div.title ul.links li {
1069 1069 list-style: none;
1070 1070 float: left;
1071 1071 margin: 0;
1072 1072 padding: 0;
1073 1073 }
1074 1074
1075 1075 #content div.box div.title ul.links li a {
1076 1076 border-left: 1px solid #316293;
1077 1077 color: #FFFFFF;
1078 1078 display: block;
1079 1079 float: left;
1080 1080 font-size: 13px;
1081 1081 font-weight: 700;
1082 1082 height: 1%;
1083 1083 margin: 0;
1084 1084 padding: 11px 22px 12px;
1085 1085 text-decoration: none;
1086 1086 }
1087 1087
1088 1088 #content div.box h1,#content div.box h2,#content div.box h3,#content div.box h4,#content div.box h5,#content div.box h6
1089 1089 {
1090 1090 clear: both;
1091 1091 overflow: hidden;
1092 1092 border-bottom: 1px solid #DDD;
1093 1093 margin: 10px 20px;
1094 1094 padding: 0 0 15px;
1095 1095 }
1096 1096
1097 1097 #content div.box p {
1098 1098 color: #5f5f5f;
1099 1099 font-size: 12px;
1100 1100 line-height: 150%;
1101 1101 margin: 0 24px 10px;
1102 1102 padding: 0;
1103 1103 }
1104 1104
1105 1105 #content div.box blockquote {
1106 1106 border-left: 4px solid #DDD;
1107 1107 color: #5f5f5f;
1108 1108 font-size: 11px;
1109 1109 line-height: 150%;
1110 1110 margin: 0 34px;
1111 1111 padding: 0 0 0 14px;
1112 1112 }
1113 1113
1114 1114 #content div.box blockquote p {
1115 1115 margin: 10px 0;
1116 1116 padding: 0;
1117 1117 }
1118 1118
1119 1119 #content div.box dl {
1120 1120 margin: 10px 0px;
1121 1121 }
1122 1122
1123 1123 #content div.box dt {
1124 1124 font-size: 12px;
1125 1125 margin: 0;
1126 1126 }
1127 1127
1128 1128 #content div.box dd {
1129 1129 font-size: 12px;
1130 1130 margin: 0;
1131 1131 padding: 8px 0 8px 15px;
1132 1132 }
1133 1133
1134 1134 #content div.box li {
1135 1135 font-size: 12px;
1136 1136 padding: 4px 0;
1137 1137 }
1138 1138
1139 1139 #content div.box ul.disc,#content div.box ul.circle {
1140 1140 margin: 10px 24px 10px 38px;
1141 1141 }
1142 1142
1143 1143 #content div.box ul.square {
1144 1144 margin: 10px 24px 10px 40px;
1145 1145 }
1146 1146
1147 1147 #content div.box img.left {
1148 1148 border: none;
1149 1149 float: left;
1150 1150 margin: 10px 10px 10px 0;
1151 1151 }
1152 1152
1153 1153 #content div.box img.right {
1154 1154 border: none;
1155 1155 float: right;
1156 1156 margin: 10px 0 10px 10px;
1157 1157 }
1158 1158
1159 1159 #content div.box div.messages {
1160 1160 clear: both;
1161 1161 overflow: hidden;
1162 1162 margin: 0 20px;
1163 1163 padding: 0;
1164 1164 }
1165 1165
1166 1166 #content div.box div.message {
1167 1167 clear: both;
1168 1168 overflow: hidden;
1169 1169 margin: 0;
1170 1170 padding: 5px 0;
1171 1171 white-space: pre-wrap;
1172 1172 }
1173 1173 #content div.box div.expand {
1174 1174 width: 110%;
1175 1175 height:14px;
1176 1176 font-size:10px;
1177 1177 text-align:center;
1178 1178 cursor: pointer;
1179 1179 color:#666;
1180 1180
1181 1181 background:-webkit-gradient(linear,0% 50%,100% 50%,color-stop(0%,rgba(255,255,255,0)),color-stop(100%,rgba(64,96,128,0.1)));
1182 1182 background:-webkit-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1183 1183 background:-moz-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1184 1184 background:-o-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1185 1185 background:-ms-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1186 1186 background:linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1187 1187
1188 1188 display: none;
1189 1189 }
1190 1190 #content div.box div.expand .expandtext {
1191 1191 background-color: #ffffff;
1192 1192 padding: 2px;
1193 1193 border-radius: 2px;
1194 1194 }
1195 1195
1196 1196 #content div.box div.message a {
1197 1197 font-weight: 400 !important;
1198 1198 }
1199 1199
1200 1200 #content div.box div.message div.image {
1201 1201 float: left;
1202 1202 margin: 9px 0 0 5px;
1203 1203 padding: 6px;
1204 1204 }
1205 1205
1206 1206 #content div.box div.message div.image img {
1207 1207 vertical-align: middle;
1208 1208 margin: 0;
1209 1209 }
1210 1210
1211 1211 #content div.box div.message div.text {
1212 1212 float: left;
1213 1213 margin: 0;
1214 1214 padding: 9px 6px;
1215 1215 }
1216 1216
1217 1217 #content div.box div.message div.dismiss a {
1218 1218 height: 16px;
1219 1219 width: 16px;
1220 1220 display: block;
1221 1221 background: url("../images/icons/cross.png") no-repeat;
1222 1222 margin: 15px 14px 0 0;
1223 1223 padding: 0;
1224 1224 }
1225 1225
1226 1226 #content div.box div.message div.text h1,#content div.box div.message div.text h2,#content div.box div.message div.text h3,#content div.box div.message div.text h4,#content div.box div.message div.text h5,#content div.box div.message div.text h6
1227 1227 {
1228 1228 border: none;
1229 1229 margin: 0;
1230 1230 padding: 0;
1231 1231 }
1232 1232
1233 1233 #content div.box div.message div.text span {
1234 1234 height: 1%;
1235 1235 display: block;
1236 1236 margin: 0;
1237 1237 padding: 5px 0 0;
1238 1238 }
1239 1239
1240 1240 #content div.box div.message-error {
1241 1241 height: 1%;
1242 1242 clear: both;
1243 1243 overflow: hidden;
1244 1244 background: #FBE3E4;
1245 1245 border: 1px solid #FBC2C4;
1246 1246 color: #860006;
1247 1247 }
1248 1248
1249 1249 #content div.box div.message-error h6 {
1250 1250 color: #860006;
1251 1251 }
1252 1252
1253 1253 #content div.box div.message-warning {
1254 1254 height: 1%;
1255 1255 clear: both;
1256 1256 overflow: hidden;
1257 1257 background: #FFF6BF;
1258 1258 border: 1px solid #FFD324;
1259 1259 color: #5f5200;
1260 1260 }
1261 1261
1262 1262 #content div.box div.message-warning h6 {
1263 1263 color: #5f5200;
1264 1264 }
1265 1265
1266 1266 #content div.box div.message-notice {
1267 1267 height: 1%;
1268 1268 clear: both;
1269 1269 overflow: hidden;
1270 1270 background: #8FBDE0;
1271 1271 border: 1px solid #6BACDE;
1272 1272 color: #003863;
1273 1273 }
1274 1274
1275 1275 #content div.box div.message-notice h6 {
1276 1276 color: #003863;
1277 1277 }
1278 1278
1279 1279 #content div.box div.message-success {
1280 1280 height: 1%;
1281 1281 clear: both;
1282 1282 overflow: hidden;
1283 1283 background: #E6EFC2;
1284 1284 border: 1px solid #C6D880;
1285 1285 color: #4e6100;
1286 1286 }
1287 1287
1288 1288 #content div.box div.message-success h6 {
1289 1289 color: #4e6100;
1290 1290 }
1291 1291
1292 1292 #content div.box div.form div.fields div.field {
1293 1293 height: 1%;
1294 1294 border-bottom: 1px solid #DDD;
1295 1295 clear: both;
1296 1296 margin: 0;
1297 1297 padding: 10px 0;
1298 1298 }
1299 1299
1300 1300 #content div.box div.form div.fields div.field-first {
1301 1301 padding: 0 0 10px;
1302 1302 }
1303 1303
1304 1304 #content div.box div.form div.fields div.field-noborder {
1305 1305 border-bottom: 0 !important;
1306 1306 }
1307 1307
1308 1308 #content div.box div.form div.fields div.field span.error-message {
1309 1309 height: 1%;
1310 1310 display: inline-block;
1311 1311 color: red;
1312 1312 margin: 8px 0 0 4px;
1313 1313 padding: 0;
1314 1314 }
1315 1315
1316 1316 #content div.box div.form div.fields div.field span.success {
1317 1317 height: 1%;
1318 1318 display: block;
1319 1319 color: #316309;
1320 1320 margin: 8px 0 0;
1321 1321 padding: 0;
1322 1322 }
1323 1323
1324 1324 #content div.box div.form div.fields div.field div.label {
1325 1325 left: 70px;
1326 1326 width: 155px;
1327 1327 position: absolute;
1328 1328 margin: 0;
1329 1329 padding: 5px 0 0 0px;
1330 1330 }
1331 1331
1332 1332 #content div.box div.form div.fields div.field div.label-summary {
1333 1333 left: 30px;
1334 1334 width: 155px;
1335 1335 position: absolute;
1336 1336 margin: 0;
1337 1337 padding: 0px 0 0 0px;
1338 1338 }
1339 1339
1340 1340 #content div.box-left div.form div.fields div.field div.label,
1341 1341 #content div.box-right div.form div.fields div.field div.label,
1342 1342 #content div.box-left div.form div.fields div.field div.label,
1343 1343 #content div.box-left div.form div.fields div.field div.label-summary,
1344 1344 #content div.box-right div.form div.fields div.field div.label-summary,
1345 1345 #content div.box-left div.form div.fields div.field div.label-summary
1346 1346 {
1347 1347 clear: both;
1348 1348 overflow: hidden;
1349 1349 left: 0;
1350 1350 width: auto;
1351 1351 position: relative;
1352 1352 margin: 0;
1353 1353 padding: 0 0 8px;
1354 1354 }
1355 1355
1356 1356 #content div.box div.form div.fields div.field div.label-select {
1357 1357 padding: 5px 0 0 5px;
1358 1358 }
1359 1359
1360 1360 #content div.box-left div.form div.fields div.field div.label-select,
1361 1361 #content div.box-right div.form div.fields div.field div.label-select
1362 1362 {
1363 1363 padding: 0 0 8px;
1364 1364 }
1365 1365
1366 1366 #content div.box-left div.form div.fields div.field div.label-textarea,
1367 1367 #content div.box-right div.form div.fields div.field div.label-textarea
1368 1368 {
1369 1369 padding: 0 0 8px !important;
1370 1370 }
1371 1371
1372 1372 #content div.box div.form div.fields div.field div.label label,div.label label
1373 1373 {
1374 1374 color: #393939;
1375 1375 font-weight: 700;
1376 1376 }
1377 1377 #content div.box div.form div.fields div.field div.label label,div.label-summary label
1378 1378 {
1379 1379 color: #393939;
1380 1380 font-weight: 700;
1381 1381 }
1382 1382 #content div.box div.form div.fields div.field div.input {
1383 1383 margin: 0 0 0 200px;
1384 1384 }
1385 1385
1386 1386 #content div.box div.form div.fields div.field div.input.summary {
1387 1387 margin: 0 0 0 110px;
1388 1388 }
1389 1389 #content div.box div.form div.fields div.field div.input.summary-short {
1390 1390 margin: 0 0 0 110px;
1391 1391 }
1392 1392 #content div.box div.form div.fields div.field div.file {
1393 1393 margin: 0 0 0 200px;
1394 1394 }
1395 1395
1396 1396 #content div.box-left div.form div.fields div.field div.input,#content div.box-right div.form div.fields div.field div.input
1397 1397 {
1398 1398 margin: 0 0 0 0px;
1399 1399 }
1400 1400
1401 1401 #content div.box div.form div.fields div.field div.input input {
1402 1402 background: #FFF;
1403 1403 border-top: 1px solid #b3b3b3;
1404 1404 border-left: 1px solid #b3b3b3;
1405 1405 border-right: 1px solid #eaeaea;
1406 1406 border-bottom: 1px solid #eaeaea;
1407 1407 color: #000;
1408 1408 font-size: 11px;
1409 1409 margin: 0;
1410 1410 padding: 7px 7px 6px;
1411 1411 }
1412 1412
1413 1413 #content div.box div.form div.fields div.field div.input input#clone_url,
1414 1414 #content div.box div.form div.fields div.field div.input input#clone_url_id
1415 1415 {
1416 1416 font-size: 16px;
1417 1417 padding: 2px;
1418 1418 }
1419 1419
1420 1420 #content div.box div.form div.fields div.field div.file input {
1421 1421 background: none repeat scroll 0 0 #FFFFFF;
1422 1422 border-color: #B3B3B3 #EAEAEA #EAEAEA #B3B3B3;
1423 1423 border-style: solid;
1424 1424 border-width: 1px;
1425 1425 color: #000000;
1426 1426 font-size: 11px;
1427 1427 margin: 0;
1428 1428 padding: 7px 7px 6px;
1429 1429 }
1430 1430
1431 1431 input.disabled {
1432 1432 background-color: #F5F5F5 !important;
1433 1433 }
1434 1434 #content div.box div.form div.fields div.field div.input input.small {
1435 1435 width: 30%;
1436 1436 }
1437 1437
1438 1438 #content div.box div.form div.fields div.field div.input input.medium {
1439 1439 width: 55%;
1440 1440 }
1441 1441
1442 1442 #content div.box div.form div.fields div.field div.input input.large {
1443 1443 width: 85%;
1444 1444 }
1445 1445
1446 1446 #content div.box div.form div.fields div.field div.input input.date {
1447 1447 width: 177px;
1448 1448 }
1449 1449
1450 1450 #content div.box div.form div.fields div.field div.input input.button {
1451 1451 background: #D4D0C8;
1452 1452 border-top: 1px solid #FFF;
1453 1453 border-left: 1px solid #FFF;
1454 1454 border-right: 1px solid #404040;
1455 1455 border-bottom: 1px solid #404040;
1456 1456 color: #000;
1457 1457 margin: 0;
1458 1458 padding: 4px 8px;
1459 1459 }
1460 1460
1461 1461 #content div.box div.form div.fields div.field div.textarea {
1462 1462 border-top: 1px solid #b3b3b3;
1463 1463 border-left: 1px solid #b3b3b3;
1464 1464 border-right: 1px solid #eaeaea;
1465 1465 border-bottom: 1px solid #eaeaea;
1466 1466 margin: 0 0 0 200px;
1467 1467 padding: 10px;
1468 1468 }
1469 1469
1470 1470 #content div.box div.form div.fields div.field div.textarea-editor {
1471 1471 border: 1px solid #ddd;
1472 1472 padding: 0;
1473 1473 }
1474 1474
1475 1475 #content div.box div.form div.fields div.field div.textarea textarea {
1476 1476 width: 100%;
1477 1477 height: 220px;
1478 1478 overflow: hidden;
1479 1479 background: #FFF;
1480 1480 color: #000;
1481 1481 font-size: 11px;
1482 1482 outline: none;
1483 1483 border-width: 0;
1484 1484 margin: 0;
1485 1485 padding: 0;
1486 1486 }
1487 1487
1488 1488 #content div.box-left div.form div.fields div.field div.textarea textarea,#content div.box-right div.form div.fields div.field div.textarea textarea
1489 1489 {
1490 1490 width: 100%;
1491 1491 height: 100px;
1492 1492 }
1493 1493
1494 1494 #content div.box div.form div.fields div.field div.textarea table {
1495 1495 width: 100%;
1496 1496 border: none;
1497 1497 margin: 0;
1498 1498 padding: 0;
1499 1499 }
1500 1500
1501 1501 #content div.box div.form div.fields div.field div.textarea table td {
1502 1502 background: #DDD;
1503 1503 border: none;
1504 1504 padding: 0;
1505 1505 }
1506 1506
1507 1507 #content div.box div.form div.fields div.field div.textarea table td table
1508 1508 {
1509 1509 width: auto;
1510 1510 border: none;
1511 1511 margin: 0;
1512 1512 padding: 0;
1513 1513 }
1514 1514
1515 1515 #content div.box div.form div.fields div.field div.textarea table td table td
1516 1516 {
1517 1517 font-size: 11px;
1518 1518 padding: 5px 5px 5px 0;
1519 1519 }
1520 1520
1521 1521 #content div.box div.form div.fields div.field input[type=text]:focus,#content div.box div.form div.fields div.field input[type=password]:focus,#content div.box div.form div.fields div.field input[type=file]:focus,#content div.box div.form div.fields div.field textarea:focus,#content div.box div.form div.fields div.field select:focus
1522 1522 {
1523 1523 background: #f6f6f6;
1524 1524 border-color: #666;
1525 1525 }
1526 1526
1527 1527 div.form div.fields div.field div.button {
1528 1528 margin: 0;
1529 1529 padding: 0 0 0 8px;
1530 1530 }
1531 1531 #content div.box table.noborder {
1532 1532 border: 1px solid transparent;
1533 1533 }
1534 1534
1535 1535 #content div.box table {
1536 1536 width: 100%;
1537 1537 border-collapse: separate;
1538 1538 margin: 0;
1539 1539 padding: 0;
1540 1540 border: 1px solid #eee;
1541 1541 -webkit-border-radius: 4px;
1542 1542 -moz-border-radius: 4px;
1543 1543 border-radius: 4px;
1544 1544 }
1545 1545
1546 1546 #content div.box table th {
1547 1547 background: #eee;
1548 1548 border-bottom: 1px solid #ddd;
1549 1549 padding: 5px 0px 5px 5px;
1550 1550 }
1551 1551
1552 1552 #content div.box table th.left {
1553 1553 text-align: left;
1554 1554 }
1555 1555
1556 1556 #content div.box table th.right {
1557 1557 text-align: right;
1558 1558 }
1559 1559
1560 1560 #content div.box table th.center {
1561 1561 text-align: center;
1562 1562 }
1563 1563
1564 1564 #content div.box table th.selected {
1565 1565 vertical-align: middle;
1566 1566 padding: 0;
1567 1567 }
1568 1568
1569 1569 #content div.box table td {
1570 1570 background: #fff;
1571 1571 border-bottom: 1px solid #cdcdcd;
1572 1572 vertical-align: middle;
1573 1573 padding: 5px;
1574 1574 }
1575 1575
1576 1576 #content div.box table tr.selected td {
1577 1577 background: #FFC;
1578 1578 }
1579 1579
1580 1580 #content div.box table td.selected {
1581 1581 width: 3%;
1582 1582 text-align: center;
1583 1583 vertical-align: middle;
1584 1584 padding: 0;
1585 1585 }
1586 1586
1587 1587 #content div.box table td.action {
1588 1588 width: 45%;
1589 1589 text-align: left;
1590 1590 }
1591 1591
1592 1592 #content div.box table td.date {
1593 1593 width: 33%;
1594 1594 text-align: center;
1595 1595 }
1596 1596
1597 1597 #content div.box div.action {
1598 1598 float: right;
1599 1599 background: #FFF;
1600 1600 text-align: right;
1601 1601 margin: 10px 0 0;
1602 1602 padding: 0;
1603 1603 }
1604 1604
1605 1605 #content div.box div.action select {
1606 1606 font-size: 11px;
1607 1607 margin: 0;
1608 1608 }
1609 1609
1610 1610 #content div.box div.action .ui-selectmenu {
1611 1611 margin: 0;
1612 1612 padding: 0;
1613 1613 }
1614 1614
1615 1615 #content div.box div.pagination {
1616 1616 height: 1%;
1617 1617 clear: both;
1618 1618 overflow: hidden;
1619 1619 margin: 10px 0 0;
1620 1620 padding: 0;
1621 1621 }
1622 1622
1623 1623 #content div.box div.pagination ul.pager {
1624 1624 float: right;
1625 1625 text-align: right;
1626 1626 margin: 0;
1627 1627 padding: 0;
1628 1628 }
1629 1629
1630 1630 #content div.box div.pagination ul.pager li {
1631 1631 height: 1%;
1632 1632 float: left;
1633 1633 list-style: none;
1634 1634 background: #ebebeb url("../images/pager.png") repeat-x;
1635 1635 border-top: 1px solid #dedede;
1636 1636 border-left: 1px solid #cfcfcf;
1637 1637 border-right: 1px solid #c4c4c4;
1638 1638 border-bottom: 1px solid #c4c4c4;
1639 1639 color: #4A4A4A;
1640 1640 font-weight: 700;
1641 1641 margin: 0 0 0 4px;
1642 1642 padding: 0;
1643 1643 }
1644 1644
1645 1645 #content div.box div.pagination ul.pager li.separator {
1646 1646 padding: 6px;
1647 1647 }
1648 1648
1649 1649 #content div.box div.pagination ul.pager li.current {
1650 1650 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1651 1651 border-top: 1px solid #ccc;
1652 1652 border-left: 1px solid #bebebe;
1653 1653 border-right: 1px solid #b1b1b1;
1654 1654 border-bottom: 1px solid #afafaf;
1655 1655 color: #515151;
1656 1656 padding: 6px;
1657 1657 }
1658 1658
1659 1659 #content div.box div.pagination ul.pager li a {
1660 1660 height: 1%;
1661 1661 display: block;
1662 1662 float: left;
1663 1663 color: #515151;
1664 1664 text-decoration: none;
1665 1665 margin: 0;
1666 1666 padding: 6px;
1667 1667 }
1668 1668
1669 1669 #content div.box div.pagination ul.pager li a:hover,#content div.box div.pagination ul.pager li a:active
1670 1670 {
1671 1671 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1672 1672 border-top: 1px solid #ccc;
1673 1673 border-left: 1px solid #bebebe;
1674 1674 border-right: 1px solid #b1b1b1;
1675 1675 border-bottom: 1px solid #afafaf;
1676 1676 margin: -1px;
1677 1677 }
1678 1678
1679 1679 #content div.box div.pagination-wh {
1680 1680 height: 1%;
1681 1681 clear: both;
1682 1682 overflow: hidden;
1683 1683 text-align: right;
1684 1684 margin: 10px 0 0;
1685 1685 padding: 0;
1686 1686 }
1687 1687
1688 1688 #content div.box div.pagination-right {
1689 1689 float: right;
1690 1690 }
1691 1691
1692 1692 #content div.box div.pagination-wh a,#content div.box div.pagination-wh span.pager_dotdot
1693 1693 {
1694 1694 height: 1%;
1695 1695 float: left;
1696 1696 background: #ebebeb url("../images/pager.png") repeat-x;
1697 1697 border-top: 1px solid #dedede;
1698 1698 border-left: 1px solid #cfcfcf;
1699 1699 border-right: 1px solid #c4c4c4;
1700 1700 border-bottom: 1px solid #c4c4c4;
1701 1701 color: #4A4A4A;
1702 1702 font-weight: 700;
1703 1703 margin: 0 0 0 4px;
1704 1704 padding: 6px;
1705 1705 }
1706 1706
1707 1707 #content div.box div.pagination-wh span.pager_curpage {
1708 1708 height: 1%;
1709 1709 float: left;
1710 1710 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1711 1711 border-top: 1px solid #ccc;
1712 1712 border-left: 1px solid #bebebe;
1713 1713 border-right: 1px solid #b1b1b1;
1714 1714 border-bottom: 1px solid #afafaf;
1715 1715 color: #515151;
1716 1716 font-weight: 700;
1717 1717 margin: 0 0 0 4px;
1718 1718 padding: 6px;
1719 1719 }
1720 1720
1721 1721 #content div.box div.pagination-wh a:hover,#content div.box div.pagination-wh a:active
1722 1722 {
1723 1723 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1724 1724 border-top: 1px solid #ccc;
1725 1725 border-left: 1px solid #bebebe;
1726 1726 border-right: 1px solid #b1b1b1;
1727 1727 border-bottom: 1px solid #afafaf;
1728 1728 text-decoration: none;
1729 1729 }
1730 1730
1731 1731 #content div.box div.traffic div.legend {
1732 1732 clear: both;
1733 1733 overflow: hidden;
1734 1734 border-bottom: 1px solid #ddd;
1735 1735 margin: 0 0 10px;
1736 1736 padding: 0 0 10px;
1737 1737 }
1738 1738
1739 1739 #content div.box div.traffic div.legend h6 {
1740 1740 float: left;
1741 1741 border: none;
1742 1742 margin: 0;
1743 1743 padding: 0;
1744 1744 }
1745 1745
1746 1746 #content div.box div.traffic div.legend li {
1747 1747 list-style: none;
1748 1748 float: left;
1749 1749 font-size: 11px;
1750 1750 margin: 0;
1751 1751 padding: 0 8px 0 4px;
1752 1752 }
1753 1753
1754 1754 #content div.box div.traffic div.legend li.visits {
1755 1755 border-left: 12px solid #edc240;
1756 1756 }
1757 1757
1758 1758 #content div.box div.traffic div.legend li.pageviews {
1759 1759 border-left: 12px solid #afd8f8;
1760 1760 }
1761 1761
1762 1762 #content div.box div.traffic table {
1763 1763 width: auto;
1764 1764 }
1765 1765
1766 1766 #content div.box div.traffic table td {
1767 1767 background: transparent;
1768 1768 border: none;
1769 1769 padding: 2px 3px 3px;
1770 1770 }
1771 1771
1772 1772 #content div.box div.traffic table td.legendLabel {
1773 1773 padding: 0 3px 2px;
1774 1774 }
1775 1775
1776 1776 #summary {
1777 1777
1778 1778 }
1779 1779
1780 1780 #summary .desc {
1781 1781 white-space: pre;
1782 1782 width: 100%;
1783 1783 }
1784 1784
1785 1785 #summary .repo_name {
1786 1786 font-size: 1.6em;
1787 1787 font-weight: bold;
1788 1788 vertical-align: baseline;
1789 1789 clear: right
1790 1790 }
1791 1791
1792 1792 #footer {
1793 1793 clear: both;
1794 1794 overflow: hidden;
1795 1795 text-align: right;
1796 1796 margin: 0;
1797 1797 padding: 0 10px 4px;
1798 1798 margin: -10px 0 0;
1799 1799 }
1800 1800
1801 1801 #footer div#footer-inner {
1802 1802 background-color: #003B76;
1803 1803 background-repeat : repeat-x;
1804 1804 background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1805 1805 background-image : -moz-linear-gradient(top, #003b76, #00376e);
1806 1806 background-image : -ms-linear-gradient( top, #003b76, #00376e);
1807 1807 background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1808 1808 background-image : -webkit-linear-gradient( top, #003b76, #00376e));
1809 1809 background-image : -o-linear-gradient( top, #003b76, #00376e));
1810 1810 background-image : linear-gradient( top, #003b76, #00376e);
1811 1811 filter :progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1812 1812 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1813 1813 -webkit-border-radius: 4px 4px 4px 4px;
1814 1814 -khtml-border-radius: 4px 4px 4px 4px;
1815 1815 -moz-border-radius: 4px 4px 4px 4px;
1816 1816 border-radius: 4px 4px 4px 4px;
1817 1817 }
1818 1818
1819 1819 #footer div#footer-inner p {
1820 1820 padding: 15px 25px 15px 0;
1821 1821 color: #FFF;
1822 1822 font-weight: 700;
1823 1823 }
1824 1824
1825 1825 #footer div#footer-inner .footer-link {
1826 1826 float: left;
1827 1827 padding-left: 10px;
1828 1828 }
1829 1829
1830 1830 #footer div#footer-inner .footer-link a,#footer div#footer-inner .footer-link-right a
1831 1831 {
1832 1832 color: #FFF;
1833 1833 }
1834 1834
1835 1835 #login div.title {
1836 1836 width: 420px;
1837 1837 clear: both;
1838 1838 overflow: hidden;
1839 1839 position: relative;
1840 1840 background-color: #003B76;
1841 1841 background-repeat : repeat-x;
1842 1842 background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1843 1843 background-image : -moz-linear-gradient( top, #003b76, #00376e);
1844 1844 background-image : -ms-linear-gradient( top, #003b76, #00376e);
1845 1845 background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1846 1846 background-image : -webkit-linear-gradient( top, #003b76, #00376e));
1847 1847 background-image : -o-linear-gradient( top, #003b76, #00376e));
1848 1848 background-image : linear-gradient( top, #003b76, #00376e);
1849 1849 filter : progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1850 1850 margin: 0 auto;
1851 1851 padding: 0;
1852 1852 }
1853 1853
1854 1854 #login div.inner {
1855 1855 width: 380px;
1856 1856 background: #FFF url("../images/login.png") no-repeat top left;
1857 1857 border-top: none;
1858 1858 border-bottom: none;
1859 1859 margin: 0 auto;
1860 1860 padding: 20px;
1861 1861 }
1862 1862
1863 1863 #login div.form div.fields div.field div.label {
1864 1864 width: 173px;
1865 1865 float: left;
1866 1866 text-align: right;
1867 1867 margin: 2px 10px 0 0;
1868 1868 padding: 5px 0 0 5px;
1869 1869 }
1870 1870
1871 1871 #login div.form div.fields div.field div.input input {
1872 1872 width: 176px;
1873 1873 background: #FFF;
1874 1874 border-top: 1px solid #b3b3b3;
1875 1875 border-left: 1px solid #b3b3b3;
1876 1876 border-right: 1px solid #eaeaea;
1877 1877 border-bottom: 1px solid #eaeaea;
1878 1878 color: #000;
1879 1879 font-size: 11px;
1880 1880 margin: 0;
1881 1881 padding: 7px 7px 6px;
1882 1882 }
1883 1883
1884 1884 #login div.form div.fields div.buttons {
1885 1885 clear: both;
1886 1886 overflow: hidden;
1887 1887 border-top: 1px solid #DDD;
1888 1888 text-align: right;
1889 1889 margin: 0;
1890 1890 padding: 10px 0 0;
1891 1891 }
1892 1892
1893 1893 #login div.form div.links {
1894 1894 clear: both;
1895 1895 overflow: hidden;
1896 1896 margin: 10px 0 0;
1897 1897 padding: 0 0 2px;
1898 1898 }
1899 1899
1900 1900 .user-menu{
1901 1901 margin: 0px !important;
1902 1902 float: left;
1903 1903 }
1904 1904
1905 1905 .user-menu .container{
1906 1906 padding:0px 4px 0px 4px;
1907 1907 margin: 0px 0px 0px 0px;
1908 1908 }
1909 1909
1910 1910 .user-menu .gravatar{
1911 1911 margin: 0px 0px 0px 0px;
1912 1912 cursor: pointer;
1913 1913 }
1914 1914 .user-menu .gravatar.enabled{
1915 1915 background-color: #FDF784 !important;
1916 1916 }
1917 1917 .user-menu .gravatar:hover{
1918 1918 background-color: #FDF784 !important;
1919 1919 }
1920 1920 #quick_login{
1921 1921 min-height: 80px;
1922 1922 margin: 37px 0 0 -251px;
1923 1923 padding: 4px;
1924 1924 position: absolute;
1925 1925 width: 278px;
1926 1926
1927 1927 background-repeat: repeat-x;
1928 1928 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
1929 1929 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1930 1930 background-image: -ms-linear-gradient(top, #003b76, #00376e);
1931 1931 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
1932 1932 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
1933 1933 background-image: -o-linear-gradient(top, #003b76, #00376e);
1934 1934 background-image: linear-gradient(top, #003b76, #00376e);
1935 1935 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
1936 1936
1937 1937 z-index: 999;
1938 1938 -webkit-border-radius: 0px 0px 4px 4px;
1939 1939 -khtml-border-radius: 0px 0px 4px 4px;
1940 1940 -moz-border-radius: 0px 0px 4px 4px;
1941 1941 border-radius: 0px 0px 4px 4px;
1942 1942 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1943 1943 }
1944 1944 #quick_login h4{
1945 1945 color: #fff;
1946 1946 padding: 5px 0px 5px 14px;
1947 1947 }
1948 1948
1949 1949 #quick_login .password_forgoten {
1950 1950 padding-right: 10px;
1951 1951 padding-top: 0px;
1952 1952 text-align: left;
1953 1953 }
1954 1954
1955 1955 #quick_login .password_forgoten a {
1956 1956 font-size: 10px;
1957 1957 color: #fff;
1958 1958 }
1959 1959
1960 1960 #quick_login .register {
1961 1961 padding-right: 10px;
1962 1962 padding-top: 5px;
1963 1963 text-align: left;
1964 1964 }
1965 1965
1966 1966 #quick_login .register a {
1967 1967 font-size: 10px;
1968 1968 color: #fff;
1969 1969 }
1970 1970
1971 1971 #quick_login .submit {
1972 1972 margin: -20px 0 0 0px;
1973 1973 position: absolute;
1974 1974 right: 15px;
1975 1975 }
1976 1976
1977 1977 #quick_login .links_left{
1978 1978 float: left;
1979 1979 }
1980 1980 #quick_login .links_right{
1981 1981 float: right;
1982 1982 }
1983 1983 #quick_login .full_name{
1984 1984 color: #FFFFFF;
1985 1985 font-weight: bold;
1986 1986 padding: 3px;
1987 1987 }
1988 1988 #quick_login .big_gravatar{
1989 1989 padding:4px 0px 0px 6px;
1990 1990 }
1991 1991 #quick_login .inbox{
1992 1992 padding:4px 0px 0px 6px;
1993 1993 color: #FFFFFF;
1994 1994 font-weight: bold;
1995 1995 }
1996 1996 #quick_login .inbox a{
1997 1997 color: #FFFFFF;
1998 1998 }
1999 1999 #quick_login .email,#quick_login .email a{
2000 2000 color: #FFFFFF;
2001 2001 padding: 3px;
2002 2002
2003 2003 }
2004 2004 #quick_login .links .logout{
2005 2005
2006 2006 }
2007 2007
2008 2008 #quick_login div.form div.fields {
2009 2009 padding-top: 2px;
2010 2010 padding-left: 10px;
2011 2011 }
2012 2012
2013 2013 #quick_login div.form div.fields div.field {
2014 2014 padding: 5px;
2015 2015 }
2016 2016
2017 2017 #quick_login div.form div.fields div.field div.label label {
2018 2018 color: #fff;
2019 2019 padding-bottom: 3px;
2020 2020 }
2021 2021
2022 2022 #quick_login div.form div.fields div.field div.input input {
2023 2023 width: 236px;
2024 2024 background: #FFF;
2025 2025 border-top: 1px solid #b3b3b3;
2026 2026 border-left: 1px solid #b3b3b3;
2027 2027 border-right: 1px solid #eaeaea;
2028 2028 border-bottom: 1px solid #eaeaea;
2029 2029 color: #000;
2030 2030 font-size: 11px;
2031 2031 margin: 0;
2032 2032 padding: 5px 7px 4px;
2033 2033 }
2034 2034
2035 2035 #quick_login div.form div.fields div.buttons {
2036 2036 clear: both;
2037 2037 overflow: hidden;
2038 2038 text-align: right;
2039 2039 margin: 0;
2040 2040 padding: 5px 14px 0px 5px;
2041 2041 }
2042 2042
2043 2043 #quick_login div.form div.links {
2044 2044 clear: both;
2045 2045 overflow: hidden;
2046 2046 margin: 10px 0 0;
2047 2047 padding: 0 0 2px;
2048 2048 }
2049 2049
2050 2050 #quick_login ol.links{
2051 2051 display: block;
2052 2052 font-weight: bold;
2053 2053 list-style: none outside none;
2054 2054 text-align: right;
2055 2055 }
2056 2056 #quick_login ol.links li{
2057 2057 line-height: 27px;
2058 2058 margin: 0;
2059 2059 padding: 0;
2060 2060 color: #fff;
2061 2061 display: block;
2062 2062 float:none !important;
2063 2063 }
2064 2064
2065 2065 #quick_login ol.links li a{
2066 2066 color: #fff;
2067 2067 display: block;
2068 2068 padding: 2px;
2069 2069 }
2070 2070 #quick_login ol.links li a:HOVER{
2071 2071 background-color: inherit !important;
2072 2072 }
2073 2073
2074 2074 #register div.title {
2075 2075 clear: both;
2076 2076 overflow: hidden;
2077 2077 position: relative;
2078 2078 background-color: #003B76;
2079 2079 background-repeat: repeat-x;
2080 2080 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
2081 2081 background-image: -moz-linear-gradient(top, #003b76, #00376e);
2082 2082 background-image: -ms-linear-gradient(top, #003b76, #00376e);
2083 2083 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
2084 2084 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
2085 2085 background-image: -o-linear-gradient(top, #003b76, #00376e);
2086 2086 background-image: linear-gradient(top, #003b76, #00376e);
2087 2087 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',
2088 2088 endColorstr='#00376e', GradientType=0 );
2089 2089 margin: 0 auto;
2090 2090 padding: 0;
2091 2091 }
2092 2092
2093 2093 #register div.inner {
2094 2094 background: #FFF;
2095 2095 border-top: none;
2096 2096 border-bottom: none;
2097 2097 margin: 0 auto;
2098 2098 padding: 20px;
2099 2099 }
2100 2100
2101 2101 #register div.form div.fields div.field div.label {
2102 2102 width: 135px;
2103 2103 float: left;
2104 2104 text-align: right;
2105 2105 margin: 2px 10px 0 0;
2106 2106 padding: 5px 0 0 5px;
2107 2107 }
2108 2108
2109 2109 #register div.form div.fields div.field div.input input {
2110 2110 width: 300px;
2111 2111 background: #FFF;
2112 2112 border-top: 1px solid #b3b3b3;
2113 2113 border-left: 1px solid #b3b3b3;
2114 2114 border-right: 1px solid #eaeaea;
2115 2115 border-bottom: 1px solid #eaeaea;
2116 2116 color: #000;
2117 2117 font-size: 11px;
2118 2118 margin: 0;
2119 2119 padding: 7px 7px 6px;
2120 2120 }
2121 2121
2122 2122 #register div.form div.fields div.buttons {
2123 2123 clear: both;
2124 2124 overflow: hidden;
2125 2125 border-top: 1px solid #DDD;
2126 2126 text-align: left;
2127 2127 margin: 0;
2128 2128 padding: 10px 0 0 150px;
2129 2129 }
2130 2130
2131 2131 #register div.form div.activation_msg {
2132 2132 padding-top: 4px;
2133 2133 padding-bottom: 4px;
2134 2134 }
2135 2135
2136 2136 #journal .journal_day {
2137 2137 font-size: 20px;
2138 2138 padding: 10px 0px;
2139 2139 border-bottom: 2px solid #DDD;
2140 2140 margin-left: 10px;
2141 2141 margin-right: 10px;
2142 2142 }
2143 2143
2144 2144 #journal .journal_container {
2145 2145 padding: 5px;
2146 2146 clear: both;
2147 2147 margin: 0px 5px 0px 10px;
2148 2148 }
2149 2149
2150 2150 #journal .journal_action_container {
2151 2151 padding-left: 38px;
2152 2152 }
2153 2153
2154 2154 #journal .journal_user {
2155 2155 color: #747474;
2156 2156 font-size: 14px;
2157 2157 font-weight: bold;
2158 2158 height: 30px;
2159 2159 }
2160 2160
2161 2161 #journal .journal_icon {
2162 2162 clear: both;
2163 2163 float: left;
2164 2164 padding-right: 4px;
2165 2165 padding-top: 3px;
2166 2166 }
2167 2167
2168 2168 #journal .journal_action {
2169 2169 padding-top: 4px;
2170 2170 min-height: 2px;
2171 2171 float: left
2172 2172 }
2173 2173
2174 2174 #journal .journal_action_params {
2175 2175 clear: left;
2176 2176 padding-left: 22px;
2177 2177 }
2178 2178
2179 2179 #journal .journal_repo {
2180 2180 float: left;
2181 2181 margin-left: 6px;
2182 2182 padding-top: 3px;
2183 2183 }
2184 2184
2185 2185 #journal .date {
2186 2186 clear: both;
2187 2187 color: #777777;
2188 2188 font-size: 11px;
2189 2189 padding-left: 22px;
2190 2190 }
2191 2191
2192 2192 #journal .journal_repo .journal_repo_name {
2193 2193 font-weight: bold;
2194 2194 font-size: 1.1em;
2195 2195 }
2196 2196
2197 2197 #journal .compare_view {
2198 2198 padding: 5px 0px 5px 0px;
2199 2199 width: 95px;
2200 2200 }
2201 2201
2202 2202 .journal_highlight {
2203 2203 font-weight: bold;
2204 2204 padding: 0 2px;
2205 2205 vertical-align: bottom;
2206 2206 }
2207 2207
2208 2208 .trending_language_tbl,.trending_language_tbl td {
2209 2209 border: 0 !important;
2210 2210 margin: 0 !important;
2211 2211 padding: 0 !important;
2212 2212 }
2213 2213
2214 2214 .trending_language_tbl,.trending_language_tbl tr {
2215 2215 border-spacing: 1px;
2216 2216 }
2217 2217
2218 2218 .trending_language {
2219 2219 background-color: #003367;
2220 2220 color: #FFF;
2221 2221 display: block;
2222 2222 min-width: 20px;
2223 2223 text-decoration: none;
2224 2224 height: 12px;
2225 2225 margin-bottom: 0px;
2226 2226 margin-left: 5px;
2227 2227 white-space: pre;
2228 2228 padding: 3px;
2229 2229 }
2230 2230
2231 2231 h3.files_location {
2232 2232 font-size: 1.8em;
2233 2233 font-weight: 700;
2234 2234 border-bottom: none !important;
2235 2235 margin: 10px 0 !important;
2236 2236 }
2237 2237
2238 2238 #files_data dl dt {
2239 2239 float: left;
2240 2240 width: 60px;
2241 2241 margin: 0 !important;
2242 2242 padding: 5px;
2243 2243 }
2244 2244
2245 2245 #files_data dl dd {
2246 2246 margin: 0 !important;
2247 2247 padding: 5px !important;
2248 2248 }
2249 2249
2250 2250 .tablerow0 {
2251 2251 background-color: #F8F8F8;
2252 2252 }
2253 2253
2254 2254 .tablerow1 {
2255 2255 background-color: #FFFFFF;
2256 2256 }
2257 2257
2258 2258 .changeset_id {
2259 2259 font-family: monospace;
2260 2260 color: #666666;
2261 2261 }
2262 2262
2263 2263 .changeset_hash {
2264 2264 color: #000000;
2265 2265 }
2266 2266
2267 2267 #changeset_content {
2268 2268 border-left: 1px solid #CCC;
2269 2269 border-right: 1px solid #CCC;
2270 2270 border-bottom: 1px solid #CCC;
2271 2271 padding: 5px;
2272 2272 }
2273 2273
2274 2274 #changeset_compare_view_content {
2275 2275 border: 1px solid #CCC;
2276 2276 padding: 5px;
2277 2277 }
2278 2278
2279 2279 #changeset_content .container {
2280 2280 min-height: 100px;
2281 2281 font-size: 1.2em;
2282 2282 overflow: hidden;
2283 2283 }
2284 2284
2285 2285 #changeset_compare_view_content .compare_view_commits {
2286 2286 width: auto !important;
2287 2287 }
2288 2288
2289 2289 #changeset_compare_view_content .compare_view_commits td {
2290 2290 padding: 0px 0px 0px 12px !important;
2291 2291 }
2292 2292
2293 2293 #changeset_content .container .right {
2294 2294 float: right;
2295 2295 width: 20%;
2296 2296 text-align: right;
2297 2297 }
2298 2298
2299 2299 #changeset_content .container .left .message {
2300 2300 white-space: pre-wrap;
2301 2301 }
2302 2302 #changeset_content .container .left .message a:hover {
2303 2303 text-decoration: none;
2304 2304 }
2305 2305 .cs_files .cur_cs {
2306 2306 margin: 10px 2px;
2307 2307 font-weight: bold;
2308 2308 }
2309 2309
2310 2310 .cs_files .node {
2311 2311 float: left;
2312 2312 }
2313 2313
2314 2314 .cs_files .changes {
2315 2315 float: right;
2316 2316 color:#003367;
2317 2317
2318 2318 }
2319 2319
2320 2320 .cs_files .changes .added {
2321 2321 background-color: #BBFFBB;
2322 2322 float: left;
2323 2323 text-align: center;
2324 2324 font-size: 9px;
2325 2325 padding: 2px 0px 2px 0px;
2326 2326 }
2327 2327
2328 2328 .cs_files .changes .deleted {
2329 2329 background-color: #FF8888;
2330 2330 float: left;
2331 2331 text-align: center;
2332 2332 font-size: 9px;
2333 2333 padding: 2px 0px 2px 0px;
2334 2334 }
2335 2335
2336 2336 .cs_files .cs_added {
2337 2337 background: url("../images/icons/page_white_add.png") no-repeat scroll
2338 2338 3px;
2339 2339 height: 16px;
2340 2340 padding-left: 20px;
2341 2341 margin-top: 7px;
2342 2342 text-align: left;
2343 2343 }
2344 2344
2345 2345 .cs_files .cs_changed {
2346 2346 background: url("../images/icons/page_white_edit.png") no-repeat scroll
2347 2347 3px;
2348 2348 height: 16px;
2349 2349 padding-left: 20px;
2350 2350 margin-top: 7px;
2351 2351 text-align: left;
2352 2352 }
2353 2353
2354 2354 .cs_files .cs_removed {
2355 2355 background: url("../images/icons/page_white_delete.png") no-repeat
2356 2356 scroll 3px;
2357 2357 height: 16px;
2358 2358 padding-left: 20px;
2359 2359 margin-top: 7px;
2360 2360 text-align: left;
2361 2361 }
2362 2362
2363 2363 #graph {
2364 2364 overflow: hidden;
2365 2365 }
2366 2366
2367 2367 #graph_nodes {
2368 2368 float: left;
2369 2369 margin-right: -6px;
2370 2370 margin-top: 0px;
2371 2371 }
2372 2372
2373 2373 #graph_content {
2374 2374 width: 80%;
2375 2375 float: left;
2376 2376 }
2377 2377
2378 2378 #graph_content .container_header {
2379 2379 border-bottom: 1px solid #DDD;
2380 2380 padding: 10px;
2381 2381 height: 25px;
2382 2382 }
2383 2383
2384 2384 #graph_content #rev_range_container {
2385 2385 padding: 7px 20px;
2386 2386 float: left;
2387 2387 }
2388 2388
2389 2389 #graph_content .container {
2390 2390 border-bottom: 1px solid #DDD;
2391 2391 height: 56px;
2392 2392 overflow: hidden;
2393 2393 }
2394 2394
2395 2395 #graph_content .container .right {
2396 2396 float: right;
2397 2397 width: 23%;
2398 2398 text-align: right;
2399 2399 }
2400 2400
2401 2401 #graph_content .container .left {
2402 2402 float: left;
2403 2403 width: 25%;
2404 2404 padding-left: 5px;
2405 2405 }
2406 2406
2407 2407 #graph_content .container .mid {
2408 2408 float: left;
2409 2409 width: 49%;
2410 2410 }
2411 2411
2412 2412
2413 2413 #graph_content .container .left .date {
2414 2414 color: #666;
2415 2415 padding-left: 22px;
2416 2416 font-size: 10px;
2417 2417 }
2418 2418
2419 2419 #graph_content .container .left .author {
2420 2420 height: 22px;
2421 2421 }
2422 2422
2423 2423 #graph_content .container .left .author .user {
2424 2424 color: #444444;
2425 2425 float: left;
2426 2426 margin-left: -4px;
2427 2427 margin-top: 4px;
2428 2428 }
2429 2429
2430 2430 #graph_content .container .mid .message {
2431 2431 white-space: pre-wrap;
2432 2432 }
2433 2433
2434 2434 #graph_content .container .mid .message a:hover{
2435 2435 text-decoration: none;
2436 2436 }
2437 2437 #content #graph_content .message .revision-link,
2438 2438 #changeset_content .container .message .revision-link
2439 2439 {
2440 2440 color:#3F6F9F;
2441 2441 font-weight: bold !important;
2442 2442 }
2443 2443
2444 2444 #content #graph_content .message .issue-tracker-link,
2445 2445 #changeset_content .container .message .issue-tracker-link{
2446 2446 color:#3F6F9F;
2447 2447 font-weight: bold !important;
2448 2448 }
2449 2449
2450 2450 .right .changeset-status-container{
2451 2451 padding-right: 5px;
2452 2452 margin-top:1px;
2453 2453 float:right;
2454 2454 height:14px;
2455 2455 }
2456 2456 .code-header .changeset-status-container{
2457 2457 float:left;
2458 2458 padding:2px 0px 0px 2px;
2459 2459 }
2460 2460 .right .changeset-status-container .changeset-status-lbl{
2461 2461 color: rgb(136, 136, 136);
2462 2462 float: left;
2463 2463 padding: 0px 4px 0px 0px;
2464 2464 }
2465 2465 .code-header .changeset-status-container .changeset-status-lbl{
2466 2466 float: left;
2467 2467 padding: 0px 4px 0px 0px;
2468 2468 }
2469 2469 .right .changeset-status-container .changeset-status-ico{
2470 2470 float: left;
2471 2471 }
2472 2472 .code-header .changeset-status-container .changeset-status-ico, .container .changeset-status-ico{
2473 2473 float: left;
2474 2474 }
2475 2475 .right .comments-container{
2476 2476 padding-right: 5px;
2477 2477 margin-top:1px;
2478 2478 float:right;
2479 2479 height:14px;
2480 2480 }
2481 2481
2482 2482 .right .comments-cnt{
2483 2483 float: left;
2484 2484 color: rgb(136, 136, 136);
2485 2485 padding-right: 2px;
2486 2486 }
2487 2487
2488 2488 .right .changes{
2489 2489 clear: both;
2490 2490 }
2491 2491
2492 2492 .right .changes .changed_total {
2493 2493 display: block;
2494 2494 float: right;
2495 2495 text-align: center;
2496 2496 min-width: 45px;
2497 2497 cursor: pointer;
2498 2498 color: #444444;
2499 2499 background: #FEA;
2500 2500 -webkit-border-radius: 0px 0px 0px 6px;
2501 2501 -moz-border-radius: 0px 0px 0px 6px;
2502 2502 border-radius: 0px 0px 0px 6px;
2503 2503 padding: 1px;
2504 2504 }
2505 2505
2506 2506 .right .changes .added,.changed,.removed {
2507 2507 display: block;
2508 2508 padding: 1px;
2509 2509 color: #444444;
2510 2510 float: right;
2511 2511 text-align: center;
2512 2512 min-width: 15px;
2513 2513 }
2514 2514
2515 2515 .right .changes .added {
2516 2516 background: #CFC;
2517 2517 }
2518 2518
2519 2519 .right .changes .changed {
2520 2520 background: #FEA;
2521 2521 }
2522 2522
2523 2523 .right .changes .removed {
2524 2524 background: #FAA;
2525 2525 }
2526 2526
2527 2527 .right .merge {
2528 2528 padding: 1px 3px 1px 3px;
2529 2529 background-color: #fca062;
2530 2530 font-size: 10px;
2531 2531 font-weight: bold;
2532 2532 color: #ffffff;
2533 2533 text-transform: uppercase;
2534 2534 white-space: nowrap;
2535 2535 -webkit-border-radius: 3px;
2536 2536 -moz-border-radius: 3px;
2537 2537 border-radius: 3px;
2538 2538 margin-right: 2px;
2539 2539 }
2540 2540
2541 2541 .right .parent {
2542 2542 color: #666666;
2543 2543 clear:both;
2544 2544 }
2545 2545 .right .logtags{
2546 2546 padding: 2px 2px 2px 2px;
2547 2547 }
2548 .right .logtags .branchtag,.right .logtags .tagtag,.right .logtags .booktag{
2549 margin: 0px 2px;
2550 }
2551
2548 2552 .right .logtags .branchtag,.logtags .branchtag {
2549 2553 padding: 1px 3px 1px 3px;
2550 2554 background-color: #bfbfbf;
2551 2555 font-size: 10px;
2552 2556 font-weight: bold;
2553 2557 color: #ffffff;
2554 2558 text-transform: uppercase;
2555 2559 white-space: nowrap;
2556 2560 -webkit-border-radius: 3px;
2557 2561 -moz-border-radius: 3px;
2558 2562 border-radius: 3px;
2559 2563 }
2560 2564 .right .logtags .branchtag a:hover,.logtags .branchtag a{
2561 2565 color: #ffffff;
2562 2566 }
2563 2567 .right .logtags .branchtag a:hover,.logtags .branchtag a:hover{
2564 2568 text-decoration: none;
2565 2569 color: #ffffff;
2566 2570 }
2567 2571 .right .logtags .tagtag,.logtags .tagtag {
2568 2572 padding: 1px 3px 1px 3px;
2569 2573 background-color: #62cffc;
2570 2574 font-size: 10px;
2571 2575 font-weight: bold;
2572 2576 color: #ffffff;
2573 2577 text-transform: uppercase;
2574 2578 white-space: nowrap;
2575 2579 -webkit-border-radius: 3px;
2576 2580 -moz-border-radius: 3px;
2577 2581 border-radius: 3px;
2578 2582 }
2579 2583 .right .logtags .tagtag a:hover,.logtags .tagtag a{
2580 2584 color: #ffffff;
2581 2585 }
2582 2586 .right .logtags .tagtag a:hover,.logtags .tagtag a:hover{
2583 2587 text-decoration: none;
2584 2588 color: #ffffff;
2585 2589 }
2586 .right .logbooks .bookbook,.logbooks .bookbook {
2587 padding: 1px 3px 2px;
2590 .right .logbooks .bookbook,.logbooks .bookbook,.right .logtags .bookbook,.logtags .bookbook {
2591 padding: 1px 3px 1px 3px;
2588 2592 background-color: #46A546;
2589 font-size: 9.75px;
2593 font-size: 10px;
2590 2594 font-weight: bold;
2591 2595 color: #ffffff;
2592 2596 text-transform: uppercase;
2593 2597 white-space: nowrap;
2594 2598 -webkit-border-radius: 3px;
2595 2599 -moz-border-radius: 3px;
2596 2600 border-radius: 3px;
2597 2601 }
2598 .right .logbooks .bookbook,.logbooks .bookbook a{
2602 .right .logbooks .bookbook,.logbooks .bookbook a,.right .logtags .bookbook,.logtags .bookbook a{
2599 2603 color: #ffffff;
2600 2604 }
2601 .right .logbooks .bookbook,.logbooks .bookbook a:hover{
2605 .right .logbooks .bookbook,.logbooks .bookbook a:hover,.right .logtags .bookbook,.logtags .bookbook a:hover{
2602 2606 text-decoration: none;
2603 2607 color: #ffffff;
2604 2608 }
2605 2609 div.browserblock {
2606 2610 overflow: hidden;
2607 2611 border: 1px solid #ccc;
2608 2612 background: #f8f8f8;
2609 2613 font-size: 100%;
2610 2614 line-height: 125%;
2611 2615 padding: 0;
2612 2616 -webkit-border-radius: 6px 6px 0px 0px;
2613 2617 -moz-border-radius: 6px 6px 0px 0px;
2614 2618 border-radius: 6px 6px 0px 0px;
2615 2619 }
2616 2620
2617 2621 div.browserblock .browser-header {
2618 2622 background: #FFF;
2619 2623 padding: 10px 0px 15px 0px;
2620 2624 width: 100%;
2621 2625 }
2622 2626
2623 2627 div.browserblock .browser-nav {
2624 2628 float: left
2625 2629 }
2626 2630
2627 2631 div.browserblock .browser-branch {
2628 2632 float: left;
2629 2633 }
2630 2634
2631 2635 div.browserblock .browser-branch label {
2632 2636 color: #4A4A4A;
2633 2637 vertical-align: text-top;
2634 2638 }
2635 2639
2636 2640 div.browserblock .browser-header span {
2637 2641 margin-left: 5px;
2638 2642 font-weight: 700;
2639 2643 }
2640 2644
2641 2645 div.browserblock .browser-search {
2642 2646 clear: both;
2643 2647 padding: 8px 8px 0px 5px;
2644 2648 height: 20px;
2645 2649 }
2646 2650
2647 2651 div.browserblock #node_filter_box {
2648 2652
2649 2653 }
2650 2654
2651 2655 div.browserblock .search_activate {
2652 2656 float: left
2653 2657 }
2654 2658
2655 2659 div.browserblock .add_node {
2656 2660 float: left;
2657 2661 padding-left: 5px;
2658 2662 }
2659 2663
2660 2664 div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover
2661 2665 {
2662 2666 text-decoration: none !important;
2663 2667 }
2664 2668
2665 2669 div.browserblock .browser-body {
2666 2670 background: #EEE;
2667 2671 border-top: 1px solid #CCC;
2668 2672 }
2669 2673
2670 2674 table.code-browser {
2671 2675 border-collapse: collapse;
2672 2676 width: 100%;
2673 2677 }
2674 2678
2675 2679 table.code-browser tr {
2676 2680 margin: 3px;
2677 2681 }
2678 2682
2679 2683 table.code-browser thead th {
2680 2684 background-color: #EEE;
2681 2685 height: 20px;
2682 2686 font-size: 1.1em;
2683 2687 font-weight: 700;
2684 2688 text-align: left;
2685 2689 padding-left: 10px;
2686 2690 }
2687 2691
2688 2692 table.code-browser tbody td {
2689 2693 padding-left: 10px;
2690 2694 height: 20px;
2691 2695 }
2692 2696
2693 2697 table.code-browser .browser-file {
2694 2698 background: url("../images/icons/document_16.png") no-repeat scroll 3px;
2695 2699 height: 16px;
2696 2700 padding-left: 20px;
2697 2701 text-align: left;
2698 2702 }
2699 2703 .diffblock .changeset_header {
2700 2704 height: 16px;
2701 2705 }
2702 2706 .diffblock .changeset_file {
2703 2707 background: url("../images/icons/file.png") no-repeat scroll 3px;
2704 2708 text-align: left;
2705 2709 float: left;
2706 2710 padding: 2px 0px 2px 22px;
2707 2711 }
2708 2712 .diffblock .diff-menu-wrapper{
2709 2713 float: left;
2710 2714 }
2711 2715
2712 2716 .diffblock .diff-menu{
2713 2717 position: absolute;
2714 2718 background: none repeat scroll 0 0 #FFFFFF;
2715 2719 border-color: #003367 #666666 #666666;
2716 2720 border-right: 1px solid #666666;
2717 2721 border-style: solid solid solid;
2718 2722 border-width: 1px;
2719 2723 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
2720 2724 margin-top:5px;
2721 2725 margin-left:1px;
2722 2726
2723 2727 }
2724 2728 .diffblock .diff-actions {
2725 2729 padding: 2px 0px 0px 2px;
2726 2730 float: left;
2727 2731 }
2728 2732 .diffblock .diff-menu ul li {
2729 2733 padding: 0px 0px 0px 0px !important;
2730 2734 }
2731 2735 .diffblock .diff-menu ul li a{
2732 2736 display: block;
2733 2737 padding: 3px 8px 3px 8px !important;
2734 2738 }
2735 2739 .diffblock .diff-menu ul li a:hover{
2736 2740 text-decoration: none;
2737 2741 background-color: #EEEEEE;
2738 2742 }
2739 2743 table.code-browser .browser-dir {
2740 2744 background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
2741 2745 height: 16px;
2742 2746 padding-left: 20px;
2743 2747 text-align: left;
2744 2748 }
2745 2749
2746 2750 table.code-browser .submodule-dir {
2747 2751 background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
2748 2752 height: 16px;
2749 2753 padding-left: 20px;
2750 2754 text-align: left;
2751 2755 }
2752 2756
2753 2757
2754 2758 .box .search {
2755 2759 clear: both;
2756 2760 overflow: hidden;
2757 2761 margin: 0;
2758 2762 padding: 0 20px 10px;
2759 2763 }
2760 2764
2761 2765 .box .search div.search_path {
2762 2766 background: none repeat scroll 0 0 #EEE;
2763 2767 border: 1px solid #CCC;
2764 2768 color: blue;
2765 2769 margin-bottom: 10px;
2766 2770 padding: 10px 0;
2767 2771 }
2768 2772
2769 2773 .box .search div.search_path div.link {
2770 2774 font-weight: 700;
2771 2775 margin-left: 25px;
2772 2776 }
2773 2777
2774 2778 .box .search div.search_path div.link a {
2775 2779 color: #003367;
2776 2780 cursor: pointer;
2777 2781 text-decoration: none;
2778 2782 }
2779 2783
2780 2784 #path_unlock {
2781 2785 color: red;
2782 2786 font-size: 1.2em;
2783 2787 padding-left: 4px;
2784 2788 }
2785 2789
2786 2790 .info_box span {
2787 2791 margin-left: 3px;
2788 2792 margin-right: 3px;
2789 2793 }
2790 2794
2791 2795 .info_box .rev {
2792 2796 color: #003367;
2793 2797 font-size: 1.6em;
2794 2798 font-weight: bold;
2795 2799 vertical-align: sub;
2796 2800 }
2797 2801
2798 2802 .info_box input#at_rev,.info_box input#size {
2799 2803 background: #FFF;
2800 2804 border-top: 1px solid #b3b3b3;
2801 2805 border-left: 1px solid #b3b3b3;
2802 2806 border-right: 1px solid #eaeaea;
2803 2807 border-bottom: 1px solid #eaeaea;
2804 2808 color: #000;
2805 2809 font-size: 12px;
2806 2810 margin: 0;
2807 2811 padding: 1px 5px 1px;
2808 2812 }
2809 2813
2810 2814 .info_box input#view {
2811 2815 text-align: center;
2812 2816 padding: 4px 3px 2px 2px;
2813 2817 }
2814 2818
2815 2819 .yui-overlay,.yui-panel-container {
2816 2820 visibility: hidden;
2817 2821 position: absolute;
2818 2822 z-index: 2;
2819 2823 }
2820 2824
2821 2825 .yui-tt {
2822 2826 visibility: hidden;
2823 2827 position: absolute;
2824 2828 color: #666;
2825 2829 background-color: #FFF;
2826 2830 border: 2px solid #003367;
2827 2831 font: 100% sans-serif;
2828 2832 width: auto;
2829 2833 opacity: 1px;
2830 2834 padding: 8px;
2831 2835 white-space: pre-wrap;
2832 2836 -webkit-border-radius: 8px 8px 8px 8px;
2833 2837 -khtml-border-radius: 8px 8px 8px 8px;
2834 2838 -moz-border-radius: 8px 8px 8px 8px;
2835 2839 border-radius: 8px 8px 8px 8px;
2836 2840 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
2837 2841 }
2838 2842
2839 2843 .ac {
2840 2844 vertical-align: top;
2841 2845 }
2842 2846
2843 2847 .ac .yui-ac {
2844 2848 position: inherit;
2845 2849 font-size: 100%;
2846 2850 }
2847 2851
2848 2852 .ac .perm_ac {
2849 2853 width: 20em;
2850 2854 }
2851 2855
2852 2856 .ac .yui-ac-input {
2853 2857 width: 100%;
2854 2858 }
2855 2859
2856 2860 .ac .yui-ac-container {
2857 2861 position: absolute;
2858 2862 top: 1.6em;
2859 2863 width: auto;
2860 2864 }
2861 2865
2862 2866 .ac .yui-ac-content {
2863 2867 position: absolute;
2864 2868 border: 1px solid gray;
2865 2869 background: #fff;
2866 2870 z-index: 9050;
2867 2871
2868 2872 }
2869 2873
2870 2874 .ac .yui-ac-shadow {
2871 2875 position: absolute;
2872 2876 width: 100%;
2873 2877 background: #000;
2874 2878 -moz-opacity: 0.1px;
2875 2879 opacity: .10;
2876 2880 filter: alpha(opacity = 10);
2877 2881 z-index: 9049;
2878 2882 margin: .3em;
2879 2883 }
2880 2884
2881 2885 .ac .yui-ac-content ul {
2882 2886 width: 100%;
2883 2887 margin: 0;
2884 2888 padding: 0;
2885 2889 z-index: 9050;
2886 2890 }
2887 2891
2888 2892 .ac .yui-ac-content li {
2889 2893 cursor: default;
2890 2894 white-space: nowrap;
2891 2895 margin: 0;
2892 2896 padding: 2px 5px;
2893 2897 height: 18px;
2894 2898 z-index: 9050;
2895 2899 display: block;
2896 2900 width: auto !important;
2897 2901 }
2898 2902
2899 2903 .ac .yui-ac-content li .ac-container-wrap{
2900 2904 width: auto;
2901 2905 }
2902 2906
2903 2907 .ac .yui-ac-content li.yui-ac-prehighlight {
2904 2908 background: #B3D4FF;
2905 2909 z-index: 9050;
2906 2910 }
2907 2911
2908 2912 .ac .yui-ac-content li.yui-ac-highlight {
2909 2913 background: #556CB5;
2910 2914 color: #FFF;
2911 2915 z-index: 9050;
2912 2916 }
2913 2917 .ac .yui-ac-bd{
2914 2918 z-index: 9050;
2915 2919 }
2916 2920
2917 2921 .follow {
2918 2922 background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
2919 2923 height: 16px;
2920 2924 width: 20px;
2921 2925 cursor: pointer;
2922 2926 display: block;
2923 2927 float: right;
2924 2928 margin-top: 2px;
2925 2929 }
2926 2930
2927 2931 .following {
2928 2932 background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
2929 2933 height: 16px;
2930 2934 width: 20px;
2931 2935 cursor: pointer;
2932 2936 display: block;
2933 2937 float: right;
2934 2938 margin-top: 2px;
2935 2939 }
2936 2940
2937 2941 .currently_following {
2938 2942 padding-left: 10px;
2939 2943 padding-bottom: 5px;
2940 2944 }
2941 2945
2942 2946 .add_icon {
2943 2947 background: url("../images/icons/add.png") no-repeat scroll 3px;
2944 2948 padding-left: 20px;
2945 2949 padding-top: 0px;
2946 2950 text-align: left;
2947 2951 }
2948 2952
2949 2953 .edit_icon {
2950 2954 background: url("../images/icons/folder_edit.png") no-repeat scroll 3px;
2951 2955 padding-left: 20px;
2952 2956 padding-top: 0px;
2953 2957 text-align: left;
2954 2958 }
2955 2959
2956 2960 .delete_icon {
2957 2961 background: url("../images/icons/delete.png") no-repeat scroll 3px;
2958 2962 padding-left: 20px;
2959 2963 padding-top: 0px;
2960 2964 text-align: left;
2961 2965 }
2962 2966
2963 2967 .refresh_icon {
2964 2968 background: url("../images/icons/arrow_refresh.png") no-repeat scroll
2965 2969 3px;
2966 2970 padding-left: 20px;
2967 2971 padding-top: 0px;
2968 2972 text-align: left;
2969 2973 }
2970 2974
2971 2975 .pull_icon {
2972 2976 background: url("../images/icons/connect.png") no-repeat scroll 3px;
2973 2977 padding-left: 20px;
2974 2978 padding-top: 0px;
2975 2979 text-align: left;
2976 2980 }
2977 2981
2978 2982 .rss_icon {
2979 2983 background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
2980 2984 padding-left: 20px;
2981 2985 padding-top: 4px;
2982 2986 text-align: left;
2983 2987 font-size: 8px
2984 2988 }
2985 2989
2986 2990 .atom_icon {
2987 2991 background: url("../images/icons/atom.png") no-repeat scroll 3px;
2988 2992 padding-left: 20px;
2989 2993 padding-top: 4px;
2990 2994 text-align: left;
2991 2995 font-size: 8px
2992 2996 }
2993 2997
2994 2998 .archive_icon {
2995 2999 background: url("../images/icons/compress.png") no-repeat scroll 3px;
2996 3000 padding-left: 20px;
2997 3001 text-align: left;
2998 3002 padding-top: 1px;
2999 3003 }
3000 3004
3001 3005 .start_following_icon {
3002 3006 background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
3003 3007 padding-left: 20px;
3004 3008 text-align: left;
3005 3009 padding-top: 0px;
3006 3010 }
3007 3011
3008 3012 .stop_following_icon {
3009 3013 background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
3010 3014 padding-left: 20px;
3011 3015 text-align: left;
3012 3016 padding-top: 0px;
3013 3017 }
3014 3018
3015 3019 .action_button {
3016 3020 border: 0;
3017 3021 display: inline;
3018 3022 }
3019 3023
3020 3024 .action_button:hover {
3021 3025 border: 0;
3022 3026 text-decoration: underline;
3023 3027 cursor: pointer;
3024 3028 }
3025 3029
3026 3030 #switch_repos {
3027 3031 position: absolute;
3028 3032 height: 25px;
3029 3033 z-index: 1;
3030 3034 }
3031 3035
3032 3036 #switch_repos select {
3033 3037 min-width: 150px;
3034 3038 max-height: 250px;
3035 3039 z-index: 1;
3036 3040 }
3037 3041
3038 3042 .breadcrumbs {
3039 3043 border: medium none;
3040 3044 color: #FFF;
3041 3045 float: left;
3042 3046 text-transform: uppercase;
3043 3047 font-weight: 700;
3044 3048 font-size: 14px;
3045 3049 margin: 0;
3046 3050 padding: 11px 0 11px 10px;
3047 3051 }
3048 3052
3049 3053 .breadcrumbs .hash {
3050 3054 text-transform: none;
3051 3055 color: #fff;
3052 3056 }
3053 3057
3054 3058 .breadcrumbs a {
3055 3059 color: #FFF;
3056 3060 }
3057 3061
3058 3062 .flash_msg {
3059 3063
3060 3064 }
3061 3065
3062 3066 .flash_msg ul {
3063 3067
3064 3068 }
3065 3069
3066 3070 .error_msg {
3067 3071 background-color: #c43c35;
3068 3072 background-repeat: repeat-x;
3069 3073 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35) );
3070 3074 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3071 3075 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3072 3076 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35) );
3073 3077 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3074 3078 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3075 3079 background-image: linear-gradient(top, #ee5f5b, #c43c35);
3076 3080 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35', GradientType=0 );
3077 3081 border-color: #c43c35 #c43c35 #882a25;
3078 3082 }
3079 3083
3080 3084 .warning_msg {
3081 3085 color: #404040 !important;
3082 3086 background-color: #eedc94;
3083 3087 background-repeat: repeat-x;
3084 3088 background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) );
3085 3089 background-image: -moz-linear-gradient(top, #fceec1, #eedc94);
3086 3090 background-image: -ms-linear-gradient(top, #fceec1, #eedc94);
3087 3091 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94) );
3088 3092 background-image: -webkit-linear-gradient(top, #fceec1, #eedc94);
3089 3093 background-image: -o-linear-gradient(top, #fceec1, #eedc94);
3090 3094 background-image: linear-gradient(top, #fceec1, #eedc94);
3091 3095 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0 );
3092 3096 border-color: #eedc94 #eedc94 #e4c652;
3093 3097 }
3094 3098
3095 3099 .success_msg {
3096 3100 background-color: #57a957;
3097 3101 background-repeat: repeat-x !important;
3098 3102 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957) );
3099 3103 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3100 3104 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3101 3105 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957) );
3102 3106 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3103 3107 background-image: -o-linear-gradient(top, #62c462, #57a957);
3104 3108 background-image: linear-gradient(top, #62c462, #57a957);
3105 3109 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0 );
3106 3110 border-color: #57a957 #57a957 #3d773d;
3107 3111 }
3108 3112
3109 3113 .notice_msg {
3110 3114 background-color: #339bb9;
3111 3115 background-repeat: repeat-x;
3112 3116 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9) );
3113 3117 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3114 3118 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3115 3119 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9) );
3116 3120 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3117 3121 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3118 3122 background-image: linear-gradient(top, #5bc0de, #339bb9);
3119 3123 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0 );
3120 3124 border-color: #339bb9 #339bb9 #22697d;
3121 3125 }
3122 3126
3123 3127 .success_msg,.error_msg,.notice_msg,.warning_msg {
3124 3128 font-size: 12px;
3125 3129 font-weight: 700;
3126 3130 min-height: 14px;
3127 3131 line-height: 14px;
3128 3132 margin-bottom: 10px;
3129 3133 margin-top: 0;
3130 3134 display: block;
3131 3135 overflow: auto;
3132 3136 padding: 6px 10px 6px 10px;
3133 3137 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3134 3138 position: relative;
3135 3139 color: #FFF;
3136 3140 border-width: 1px;
3137 3141 border-style: solid;
3138 3142 -webkit-border-radius: 4px;
3139 3143 -moz-border-radius: 4px;
3140 3144 border-radius: 4px;
3141 3145 -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3142 3146 -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3143 3147 box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3144 3148 }
3145 3149
3146 3150 #msg_close {
3147 3151 background: transparent url("../icons/cross_grey_small.png") no-repeat scroll 0 0;
3148 3152 cursor: pointer;
3149 3153 height: 16px;
3150 3154 position: absolute;
3151 3155 right: 5px;
3152 3156 top: 5px;
3153 3157 width: 16px;
3154 3158 }
3155 3159 div#legend_data{
3156 3160 padding-left:10px;
3157 3161 }
3158 3162 div#legend_container table{
3159 3163 border: none !important;
3160 3164 }
3161 3165 div#legend_container table,div#legend_choices table {
3162 3166 width: auto !important;
3163 3167 }
3164 3168
3165 3169 table#permissions_manage {
3166 3170 width: 0 !important;
3167 3171 }
3168 3172
3169 3173 table#permissions_manage span.private_repo_msg {
3170 3174 font-size: 0.8em;
3171 3175 opacity: 0.6px;
3172 3176 }
3173 3177
3174 3178 table#permissions_manage td.private_repo_msg {
3175 3179 font-size: 0.8em;
3176 3180 }
3177 3181
3178 3182 table#permissions_manage tr#add_perm_input td {
3179 3183 vertical-align: middle;
3180 3184 }
3181 3185
3182 3186 div.gravatar {
3183 3187 background-color: #FFF;
3184 3188 float: left;
3185 3189 margin-right: 0.7em;
3186 3190 padding: 1px 1px 1px 1px;
3187 3191 line-height:0;
3188 3192 -webkit-border-radius: 3px;
3189 3193 -khtml-border-radius: 3px;
3190 3194 -moz-border-radius: 3px;
3191 3195 border-radius: 3px;
3192 3196 }
3193 3197
3194 3198 div.gravatar img {
3195 3199 -webkit-border-radius: 2px;
3196 3200 -khtml-border-radius: 2px;
3197 3201 -moz-border-radius: 2px;
3198 3202 border-radius: 2px;
3199 3203 }
3200 3204
3201 3205 #header,#content,#footer {
3202 3206 min-width: 978px;
3203 3207 }
3204 3208
3205 3209 #content {
3206 3210 clear: both;
3207 3211 overflow: hidden;
3208 3212 padding: 54px 10px 14px 10px;
3209 3213 }
3210 3214
3211 3215 #content div.box div.title div.search {
3212 3216
3213 3217 border-left: 1px solid #316293;
3214 3218 }
3215 3219
3216 3220 #content div.box div.title div.search div.input input {
3217 3221 border: 1px solid #316293;
3218 3222 }
3219 3223
3220 3224 .ui-btn{
3221 3225 color: #515151;
3222 3226 background-color: #DADADA;
3223 3227 background-repeat: repeat-x;
3224 3228 background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) );
3225 3229 background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA);
3226 3230 background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA);
3227 3231 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) );
3228 3232 background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) );
3229 3233 background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) );
3230 3234 background-image: linear-gradient(top, #F4F4F4, #DADADA);
3231 3235 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0);
3232 3236
3233 3237 border-top: 1px solid #DDD;
3234 3238 border-left: 1px solid #c6c6c6;
3235 3239 border-right: 1px solid #DDD;
3236 3240 border-bottom: 1px solid #c6c6c6;
3237 3241 color: #515151;
3238 3242 outline: none;
3239 3243 margin: 0px 3px 3px 0px;
3240 3244 -webkit-border-radius: 4px 4px 4px 4px !important;
3241 3245 -khtml-border-radius: 4px 4px 4px 4px !important;
3242 3246 -moz-border-radius: 4px 4px 4px 4px !important;
3243 3247 border-radius: 4px 4px 4px 4px !important;
3244 3248 cursor: pointer !important;
3245 3249 padding: 3px 3px 3px 3px;
3246 3250 background-position: 0 -15px;
3247 3251
3248 3252 }
3249 3253 .ui-btn.xsmall{
3250 3254 padding: 1px 2px 1px 1px;
3251 3255 }
3252 3256 .ui-btn.clone{
3253 3257 padding: 5px 2px 6px 1px;
3254 3258 margin: 0px -4px 3px 0px;
3255 3259 -webkit-border-radius: 4px 0px 0px 4px !important;
3256 3260 -khtml-border-radius: 4px 0px 0px 4px !important;
3257 3261 -moz-border-radius: 4px 0px 0px 4px !important;
3258 3262 border-radius: 4px 0px 0px 4px !important;
3259 3263 width: 100px;
3260 3264 text-align: center;
3261 3265 float: left;
3262 3266 position: absolute;
3263 3267 }
3264 3268 .ui-btn:focus {
3265 3269 outline: none;
3266 3270 }
3267 3271 .ui-btn:hover{
3268 3272 background-position: 0 0px;
3269 3273 text-decoration: none;
3270 3274 color: #515151;
3271 3275 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
3272 3276 }
3273 3277
3274 3278 .ui-btn.red{
3275 3279 color:#fff;
3276 3280 background-color: #c43c35;
3277 3281 background-repeat: repeat-x;
3278 3282 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
3279 3283 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3280 3284 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3281 3285 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
3282 3286 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3283 3287 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3284 3288 background-image: linear-gradient(top, #ee5f5b, #c43c35);
3285 3289 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
3286 3290 border-color: #c43c35 #c43c35 #882a25;
3287 3291 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3288 3292 }
3289 3293
3290 3294
3291 3295 .ui-btn.blue{
3292 3296 background-color: #339bb9;
3293 3297 background-repeat: repeat-x;
3294 3298 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
3295 3299 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3296 3300 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3297 3301 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
3298 3302 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3299 3303 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3300 3304 background-image: linear-gradient(top, #5bc0de, #339bb9);
3301 3305 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
3302 3306 border-color: #339bb9 #339bb9 #22697d;
3303 3307 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3304 3308 }
3305 3309
3306 3310 .ui-btn.green{
3307 3311 background-color: #57a957;
3308 3312 background-repeat: repeat-x;
3309 3313 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
3310 3314 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3311 3315 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3312 3316 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
3313 3317 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3314 3318 background-image: -o-linear-gradient(top, #62c462, #57a957);
3315 3319 background-image: linear-gradient(top, #62c462, #57a957);
3316 3320 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
3317 3321 border-color: #57a957 #57a957 #3d773d;
3318 3322 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3319 3323 }
3320 3324
3321 3325 ins,div.options a:hover {
3322 3326 text-decoration: none;
3323 3327 }
3324 3328
3325 3329 img,
3326 3330 #header #header-inner #quick li a:hover span.normal,
3327 3331 #header #header-inner #quick li ul li.last,
3328 3332 #content div.box div.form div.fields div.field div.textarea table td table td a,
3329 3333 #clone_url,
3330 3334 #clone_url_id
3331 3335 {
3332 3336 border: none;
3333 3337 }
3334 3338
3335 3339 img.icon,.right .merge img {
3336 3340 vertical-align: bottom;
3337 3341 }
3338 3342
3339 3343 #header ul#logged-user,#content div.box div.title ul.links,
3340 3344 #content div.box div.message div.dismiss,
3341 3345 #content div.box div.traffic div.legend ul
3342 3346 {
3343 3347 float: right;
3344 3348 margin: 0;
3345 3349 padding: 0;
3346 3350 }
3347 3351
3348 3352 #header #header-inner #home,#header #header-inner #logo,
3349 3353 #content div.box ul.left,#content div.box ol.left,
3350 3354 #content div.box div.pagination-left,div#commit_history,
3351 3355 div#legend_data,div#legend_container,div#legend_choices
3352 3356 {
3353 3357 float: left;
3354 3358 }
3355 3359
3356 3360 #header #header-inner #quick li:hover ul ul,
3357 3361 #header #header-inner #quick li:hover ul ul ul,
3358 3362 #header #header-inner #quick li:hover ul ul ul ul,
3359 3363 #content #left #menu ul.closed,#content #left #menu li ul.collapsed,.yui-tt-shadow
3360 3364 {
3361 3365 display: none;
3362 3366 }
3363 3367
3364 3368 #header #header-inner #quick li:hover ul,#header #header-inner #quick li li:hover ul,#header #header-inner #quick li li li:hover ul,#header #header-inner #quick li li li li:hover ul,#content #left #menu ul.opened,#content #left #menu li ul.expanded
3365 3369 {
3366 3370 display: block;
3367 3371 }
3368 3372
3369 3373 #content div.graph {
3370 3374 padding: 0 10px 10px;
3371 3375 }
3372 3376
3373 3377 #content div.box div.title ul.links li a:hover,#content div.box div.title ul.links li.ui-tabs-selected a
3374 3378 {
3375 3379 color: #bfe3ff;
3376 3380 }
3377 3381
3378 3382 #content div.box ol.lower-roman,#content div.box ol.upper-roman,#content div.box ol.lower-alpha,#content div.box ol.upper-alpha,#content div.box ol.decimal
3379 3383 {
3380 3384 margin: 10px 24px 10px 44px;
3381 3385 }
3382 3386
3383 3387 #content div.box div.form,#content div.box div.table,#content div.box div.traffic
3384 3388 {
3385 3389 clear: both;
3386 3390 overflow: hidden;
3387 3391 margin: 0;
3388 3392 padding: 0 20px 10px;
3389 3393 }
3390 3394
3391 3395 #content div.box div.form div.fields,#login div.form,#login div.form div.fields,#register div.form,#register div.form div.fields
3392 3396 {
3393 3397 clear: both;
3394 3398 overflow: hidden;
3395 3399 margin: 0;
3396 3400 padding: 0;
3397 3401 }
3398 3402
3399 3403 #content div.box div.form div.fields div.field div.label span,#login div.form div.fields div.field div.label span,#register div.form div.fields div.field div.label span
3400 3404 {
3401 3405 height: 1%;
3402 3406 display: block;
3403 3407 color: #363636;
3404 3408 margin: 0;
3405 3409 padding: 2px 0 0;
3406 3410 }
3407 3411
3408 3412 #content div.box div.form div.fields div.field div.input input.error,#login div.form div.fields div.field div.input input.error,#register div.form div.fields div.field div.input input.error
3409 3413 {
3410 3414 background: #FBE3E4;
3411 3415 border-top: 1px solid #e1b2b3;
3412 3416 border-left: 1px solid #e1b2b3;
3413 3417 border-right: 1px solid #FBC2C4;
3414 3418 border-bottom: 1px solid #FBC2C4;
3415 3419 }
3416 3420
3417 3421 #content div.box div.form div.fields div.field div.input input.success,#login div.form div.fields div.field div.input input.success,#register div.form div.fields div.field div.input input.success
3418 3422 {
3419 3423 background: #E6EFC2;
3420 3424 border-top: 1px solid #cebb98;
3421 3425 border-left: 1px solid #cebb98;
3422 3426 border-right: 1px solid #c6d880;
3423 3427 border-bottom: 1px solid #c6d880;
3424 3428 }
3425 3429
3426 3430 #content div.box-left div.form div.fields div.field div.textarea,#content div.box-right div.form div.fields div.field div.textarea,#content div.box div.form div.fields div.field div.select select,#content div.box table th.selected input,#content div.box table td.selected input
3427 3431 {
3428 3432 margin: 0;
3429 3433 }
3430 3434
3431 3435 #content div.box-left div.form div.fields div.field div.select,#content div.box-left div.form div.fields div.field div.checkboxes,#content div.box-left div.form div.fields div.field div.radios,#content div.box-right div.form div.fields div.field div.select,#content div.box-right div.form div.fields div.field div.checkboxes,#content div.box-right div.form div.fields div.field div.radios
3432 3436 {
3433 3437 margin: 0 0 0 0px !important;
3434 3438 padding: 0;
3435 3439 }
3436 3440
3437 3441 #content div.box div.form div.fields div.field div.select,#content div.box div.form div.fields div.field div.checkboxes,#content div.box div.form div.fields div.field div.radios
3438 3442 {
3439 3443 margin: 0 0 0 200px;
3440 3444 padding: 0;
3441 3445 }
3442 3446
3443 3447 #content div.box div.form div.fields div.field div.select a:hover,#content div.box div.form div.fields div.field div.select a.ui-selectmenu:hover,#content div.box div.action a:hover
3444 3448 {
3445 3449 color: #000;
3446 3450 text-decoration: none;
3447 3451 }
3448 3452
3449 3453 #content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus,#content div.box div.action a.ui-selectmenu-focus
3450 3454 {
3451 3455 border: 1px solid #666;
3452 3456 }
3453 3457
3454 3458 #content div.box div.form div.fields div.field div.checkboxes div.checkbox,#content div.box div.form div.fields div.field div.radios div.radio
3455 3459 {
3456 3460 clear: both;
3457 3461 overflow: hidden;
3458 3462 margin: 0;
3459 3463 padding: 8px 0 2px;
3460 3464 }
3461 3465
3462 3466 #content div.box div.form div.fields div.field div.checkboxes div.checkbox input,#content div.box div.form div.fields div.field div.radios div.radio input
3463 3467 {
3464 3468 float: left;
3465 3469 margin: 0;
3466 3470 }
3467 3471
3468 3472 #content div.box div.form div.fields div.field div.checkboxes div.checkbox label,#content div.box div.form div.fields div.field div.radios div.radio label
3469 3473 {
3470 3474 height: 1%;
3471 3475 display: block;
3472 3476 float: left;
3473 3477 margin: 2px 0 0 4px;
3474 3478 }
3475 3479
3476 3480 div.form div.fields div.field div.button input,#content div.box div.form div.fields div.buttons input,div.form div.fields div.buttons input,#content div.box div.action div.button input
3477 3481 {
3478 3482 color: #000;
3479 3483 font-size: 11px;
3480 3484 font-weight: 700;
3481 3485 margin: 0;
3482 3486 }
3483 3487
3484 3488 input.ui-button {
3485 3489 background: #e5e3e3 url("../images/button.png") repeat-x;
3486 3490 border-top: 1px solid #DDD;
3487 3491 border-left: 1px solid #c6c6c6;
3488 3492 border-right: 1px solid #DDD;
3489 3493 border-bottom: 1px solid #c6c6c6;
3490 3494 color: #515151 !important;
3491 3495 outline: none;
3492 3496 margin: 0;
3493 3497 padding: 6px 12px;
3494 3498 -webkit-border-radius: 4px 4px 4px 4px;
3495 3499 -khtml-border-radius: 4px 4px 4px 4px;
3496 3500 -moz-border-radius: 4px 4px 4px 4px;
3497 3501 border-radius: 4px 4px 4px 4px;
3498 3502 box-shadow: 0 1px 0 #ececec;
3499 3503 cursor: pointer;
3500 3504 }
3501 3505
3502 3506 input.ui-button:hover {
3503 3507 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3504 3508 border-top: 1px solid #ccc;
3505 3509 border-left: 1px solid #bebebe;
3506 3510 border-right: 1px solid #b1b1b1;
3507 3511 border-bottom: 1px solid #afafaf;
3508 3512 }
3509 3513
3510 3514 div.form div.fields div.field div.highlight,#content div.box div.form div.fields div.buttons div.highlight
3511 3515 {
3512 3516 display: inline;
3513 3517 }
3514 3518
3515 3519 #content div.box div.form div.fields div.buttons,div.form div.fields div.buttons
3516 3520 {
3517 3521 margin: 10px 0 0 200px;
3518 3522 padding: 0;
3519 3523 }
3520 3524
3521 3525 #content div.box-left div.form div.fields div.buttons,#content div.box-right div.form div.fields div.buttons,div.box-left div.form div.fields div.buttons,div.box-right div.form div.fields div.buttons
3522 3526 {
3523 3527 margin: 10px 0 0;
3524 3528 }
3525 3529
3526 3530 #content div.box table td.user,#content div.box table td.address {
3527 3531 width: 10%;
3528 3532 text-align: center;
3529 3533 }
3530 3534
3531 3535 #content div.box div.action div.button,#login div.form div.fields div.field div.input div.link,#register div.form div.fields div.field div.input div.link
3532 3536 {
3533 3537 text-align: right;
3534 3538 margin: 6px 0 0;
3535 3539 padding: 0;
3536 3540 }
3537 3541
3538 3542 #content div.box div.action div.button input.ui-state-hover,#login div.form div.fields div.buttons input.ui-state-hover,#register div.form div.fields div.buttons input.ui-state-hover
3539 3543 {
3540 3544 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3541 3545 border-top: 1px solid #ccc;
3542 3546 border-left: 1px solid #bebebe;
3543 3547 border-right: 1px solid #b1b1b1;
3544 3548 border-bottom: 1px solid #afafaf;
3545 3549 color: #515151;
3546 3550 margin: 0;
3547 3551 padding: 6px 12px;
3548 3552 }
3549 3553
3550 3554 #content div.box div.pagination div.results,#content div.box div.pagination-wh div.results
3551 3555 {
3552 3556 text-align: left;
3553 3557 float: left;
3554 3558 margin: 0;
3555 3559 padding: 0;
3556 3560 }
3557 3561
3558 3562 #content div.box div.pagination div.results span,#content div.box div.pagination-wh div.results span
3559 3563 {
3560 3564 height: 1%;
3561 3565 display: block;
3562 3566 float: left;
3563 3567 background: #ebebeb url("../images/pager.png") repeat-x;
3564 3568 border-top: 1px solid #dedede;
3565 3569 border-left: 1px solid #cfcfcf;
3566 3570 border-right: 1px solid #c4c4c4;
3567 3571 border-bottom: 1px solid #c4c4c4;
3568 3572 color: #4A4A4A;
3569 3573 font-weight: 700;
3570 3574 margin: 0;
3571 3575 padding: 6px 8px;
3572 3576 }
3573 3577
3574 3578 #content div.box div.pagination ul.pager li.disabled,#content div.box div.pagination-wh a.disabled
3575 3579 {
3576 3580 color: #B4B4B4;
3577 3581 padding: 6px;
3578 3582 }
3579 3583
3580 3584 #login,#register {
3581 3585 width: 520px;
3582 3586 margin: 10% auto 0;
3583 3587 padding: 0;
3584 3588 }
3585 3589
3586 3590 #login div.color,#register div.color {
3587 3591 clear: both;
3588 3592 overflow: hidden;
3589 3593 background: #FFF;
3590 3594 margin: 10px auto 0;
3591 3595 padding: 3px 3px 3px 0;
3592 3596 }
3593 3597
3594 3598 #login div.color a,#register div.color a {
3595 3599 width: 20px;
3596 3600 height: 20px;
3597 3601 display: block;
3598 3602 float: left;
3599 3603 margin: 0 0 0 3px;
3600 3604 padding: 0;
3601 3605 }
3602 3606
3603 3607 #login div.title h5,#register div.title h5 {
3604 3608 color: #fff;
3605 3609 margin: 10px;
3606 3610 padding: 0;
3607 3611 }
3608 3612
3609 3613 #login div.form div.fields div.field,#register div.form div.fields div.field
3610 3614 {
3611 3615 clear: both;
3612 3616 overflow: hidden;
3613 3617 margin: 0;
3614 3618 padding: 0 0 10px;
3615 3619 }
3616 3620
3617 3621 #login div.form div.fields div.field span.error-message,#register div.form div.fields div.field span.error-message
3618 3622 {
3619 3623 height: 1%;
3620 3624 display: block;
3621 3625 color: red;
3622 3626 margin: 8px 0 0;
3623 3627 padding: 0;
3624 3628 max-width: 320px;
3625 3629 }
3626 3630
3627 3631 #login div.form div.fields div.field div.label label,#register div.form div.fields div.field div.label label
3628 3632 {
3629 3633 color: #000;
3630 3634 font-weight: 700;
3631 3635 }
3632 3636
3633 3637 #login div.form div.fields div.field div.input,#register div.form div.fields div.field div.input
3634 3638 {
3635 3639 float: left;
3636 3640 margin: 0;
3637 3641 padding: 0;
3638 3642 }
3639 3643
3640 3644 #login div.form div.fields div.field div.checkbox,#register div.form div.fields div.field div.checkbox
3641 3645 {
3642 3646 margin: 0 0 0 184px;
3643 3647 padding: 0;
3644 3648 }
3645 3649
3646 3650 #login div.form div.fields div.field div.checkbox label,#register div.form div.fields div.field div.checkbox label
3647 3651 {
3648 3652 color: #565656;
3649 3653 font-weight: 700;
3650 3654 }
3651 3655
3652 3656 #login div.form div.fields div.buttons input,#register div.form div.fields div.buttons input
3653 3657 {
3654 3658 color: #000;
3655 3659 font-size: 1em;
3656 3660 font-weight: 700;
3657 3661 margin: 0;
3658 3662 }
3659 3663
3660 3664 #changeset_content .container .wrapper,#graph_content .container .wrapper
3661 3665 {
3662 3666 width: 600px;
3663 3667 }
3664 3668
3665 3669 #changeset_content .container .left {
3666 3670 float: left;
3667 3671 width: 75%;
3668 3672 padding-left: 5px;
3669 3673 }
3670 3674
3671 3675 #changeset_content .container .left .date,.ac .match {
3672 3676 font-weight: 700;
3673 3677 padding-top: 5px;
3674 3678 padding-bottom: 5px;
3675 3679 }
3676 3680
3677 3681 div#legend_container table td,div#legend_choices table td {
3678 3682 border: none !important;
3679 3683 height: 20px !important;
3680 3684 padding: 0 !important;
3681 3685 }
3682 3686
3683 3687 .q_filter_box {
3684 3688 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3685 3689 -webkit-border-radius: 4px;
3686 3690 -moz-border-radius: 4px;
3687 3691 border-radius: 4px;
3688 3692 border: 0 none;
3689 3693 color: #AAAAAA;
3690 3694 margin-bottom: -4px;
3691 3695 margin-top: -4px;
3692 3696 padding-left: 3px;
3693 3697 }
3694 3698
3695 3699 #node_filter {
3696 3700 border: 0px solid #545454;
3697 3701 color: #AAAAAA;
3698 3702 padding-left: 3px;
3699 3703 }
3700 3704
3701 3705
3702 3706 .group_members_wrap{
3703 3707
3704 3708 }
3705 3709
3706 3710 .group_members .group_member{
3707 3711 height: 30px;
3708 3712 padding:0px 0px 0px 10px;
3709 3713 }
3710 3714
3711 3715 /*README STYLE*/
3712 3716
3713 3717 div.readme {
3714 3718 padding:0px;
3715 3719 }
3716 3720
3717 3721 div.readme h2 {
3718 3722 font-weight: normal;
3719 3723 }
3720 3724
3721 3725 div.readme .readme_box {
3722 3726 background-color: #fafafa;
3723 3727 }
3724 3728
3725 3729 div.readme .readme_box {
3726 3730 clear:both;
3727 3731 overflow:hidden;
3728 3732 margin:0;
3729 3733 padding:0 20px 10px;
3730 3734 }
3731 3735
3732 3736 div.readme .readme_box h1, div.readme .readme_box h2, div.readme .readme_box h3, div.readme .readme_box h4, div.readme .readme_box h5, div.readme .readme_box h6 {
3733 3737 border-bottom: 0 !important;
3734 3738 margin: 0 !important;
3735 3739 padding: 0 !important;
3736 3740 line-height: 1.5em !important;
3737 3741 }
3738 3742
3739 3743
3740 3744 div.readme .readme_box h1:first-child {
3741 3745 padding-top: .25em !important;
3742 3746 }
3743 3747
3744 3748 div.readme .readme_box h2, div.readme .readme_box h3 {
3745 3749 margin: 1em 0 !important;
3746 3750 }
3747 3751
3748 3752 div.readme .readme_box h2 {
3749 3753 margin-top: 1.5em !important;
3750 3754 border-top: 4px solid #e0e0e0 !important;
3751 3755 padding-top: .5em !important;
3752 3756 }
3753 3757
3754 3758 div.readme .readme_box p {
3755 3759 color: black !important;
3756 3760 margin: 1em 0 !important;
3757 3761 line-height: 1.5em !important;
3758 3762 }
3759 3763
3760 3764 div.readme .readme_box ul {
3761 3765 list-style: disc !important;
3762 3766 margin: 1em 0 1em 2em !important;
3763 3767 }
3764 3768
3765 3769 div.readme .readme_box ol {
3766 3770 list-style: decimal;
3767 3771 margin: 1em 0 1em 2em !important;
3768 3772 }
3769 3773
3770 3774 div.readme .readme_box pre, code {
3771 3775 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
3772 3776 }
3773 3777
3774 3778 div.readme .readme_box code {
3775 3779 font-size: 12px !important;
3776 3780 background-color: ghostWhite !important;
3777 3781 color: #444 !important;
3778 3782 padding: 0 .2em !important;
3779 3783 border: 1px solid #dedede !important;
3780 3784 }
3781 3785
3782 3786 div.readme .readme_box pre code {
3783 3787 padding: 0 !important;
3784 3788 font-size: 12px !important;
3785 3789 background-color: #eee !important;
3786 3790 border: none !important;
3787 3791 }
3788 3792
3789 3793 div.readme .readme_box pre {
3790 3794 margin: 1em 0;
3791 3795 font-size: 12px;
3792 3796 background-color: #eee;
3793 3797 border: 1px solid #ddd;
3794 3798 padding: 5px;
3795 3799 color: #444;
3796 3800 overflow: auto;
3797 3801 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3798 3802 -webkit-border-radius: 3px;
3799 3803 -moz-border-radius: 3px;
3800 3804 border-radius: 3px;
3801 3805 }
3802 3806
3803 3807
3804 3808 /** RST STYLE **/
3805 3809
3806 3810
3807 3811 div.rst-block {
3808 3812 padding:0px;
3809 3813 }
3810 3814
3811 3815 div.rst-block h2 {
3812 3816 font-weight: normal;
3813 3817 }
3814 3818
3815 3819 div.rst-block {
3816 3820 background-color: #fafafa;
3817 3821 }
3818 3822
3819 3823 div.rst-block {
3820 3824 clear:both;
3821 3825 overflow:hidden;
3822 3826 margin:0;
3823 3827 padding:0 20px 10px;
3824 3828 }
3825 3829
3826 3830 div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
3827 3831 border-bottom: 0 !important;
3828 3832 margin: 0 !important;
3829 3833 padding: 0 !important;
3830 3834 line-height: 1.5em !important;
3831 3835 }
3832 3836
3833 3837
3834 3838 div.rst-block h1:first-child {
3835 3839 padding-top: .25em !important;
3836 3840 }
3837 3841
3838 3842 div.rst-block h2, div.rst-block h3 {
3839 3843 margin: 1em 0 !important;
3840 3844 }
3841 3845
3842 3846 div.rst-block h2 {
3843 3847 margin-top: 1.5em !important;
3844 3848 border-top: 4px solid #e0e0e0 !important;
3845 3849 padding-top: .5em !important;
3846 3850 }
3847 3851
3848 3852 div.rst-block p {
3849 3853 color: black !important;
3850 3854 margin: 1em 0 !important;
3851 3855 line-height: 1.5em !important;
3852 3856 }
3853 3857
3854 3858 div.rst-block ul {
3855 3859 list-style: disc !important;
3856 3860 margin: 1em 0 1em 2em !important;
3857 3861 }
3858 3862
3859 3863 div.rst-block ol {
3860 3864 list-style: decimal;
3861 3865 margin: 1em 0 1em 2em !important;
3862 3866 }
3863 3867
3864 3868 div.rst-block pre, code {
3865 3869 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
3866 3870 }
3867 3871
3868 3872 div.rst-block code {
3869 3873 font-size: 12px !important;
3870 3874 background-color: ghostWhite !important;
3871 3875 color: #444 !important;
3872 3876 padding: 0 .2em !important;
3873 3877 border: 1px solid #dedede !important;
3874 3878 }
3875 3879
3876 3880 div.rst-block pre code {
3877 3881 padding: 0 !important;
3878 3882 font-size: 12px !important;
3879 3883 background-color: #eee !important;
3880 3884 border: none !important;
3881 3885 }
3882 3886
3883 3887 div.rst-block pre {
3884 3888 margin: 1em 0;
3885 3889 font-size: 12px;
3886 3890 background-color: #eee;
3887 3891 border: 1px solid #ddd;
3888 3892 padding: 5px;
3889 3893 color: #444;
3890 3894 overflow: auto;
3891 3895 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3892 3896 -webkit-border-radius: 3px;
3893 3897 -moz-border-radius: 3px;
3894 3898 border-radius: 3px;
3895 3899 }
3896 3900
3897 3901
3898 3902 /** comment main **/
3899 3903 .comments {
3900 3904 padding:10px 20px;
3901 3905 }
3902 3906
3903 3907 .comments .comment {
3904 3908 border: 1px solid #ddd;
3905 3909 margin-top: 10px;
3906 3910 -webkit-border-radius: 4px;
3907 3911 -moz-border-radius: 4px;
3908 3912 border-radius: 4px;
3909 3913 }
3910 3914
3911 3915 .comments .comment .meta {
3912 3916 background: #f8f8f8;
3913 3917 padding: 4px;
3914 3918 border-bottom: 1px solid #ddd;
3915 3919 }
3916 3920
3917 3921 .comments .comment .meta img {
3918 3922 vertical-align: middle;
3919 3923 }
3920 3924
3921 3925 .comments .comment .meta .user {
3922 3926 font-weight: bold;
3923 3927 }
3924 3928
3925 3929 .comments .comment .meta .date {
3926 3930 }
3927 3931
3928 3932 .comments .comment .text {
3929 3933 background-color: #FAFAFA;
3930 3934 }
3931 3935 .comment .text div.rst-block p {
3932 3936 margin: 0.5em 0px !important;
3933 3937 }
3934 3938
3935 3939 .comments .comments-number{
3936 3940 padding:0px 0px 10px 0px;
3937 3941 font-weight: bold;
3938 3942 color: #666;
3939 3943 font-size: 16px;
3940 3944 }
3941 3945
3942 3946 /** comment form **/
3943 3947
3944 3948 .status-block{
3945 3949 height:80px;
3946 3950 clear:both
3947 3951 }
3948 3952
3949 3953 .comment-form .clearfix{
3950 3954 background: #EEE;
3951 3955 -webkit-border-radius: 4px;
3952 3956 -moz-border-radius: 4px;
3953 3957 border-radius: 4px;
3954 3958 padding: 10px;
3955 3959 }
3956 3960
3957 3961 div.comment-form {
3958 3962 margin-top: 20px;
3959 3963 }
3960 3964
3961 3965 .comment-form strong {
3962 3966 display: block;
3963 3967 margin-bottom: 15px;
3964 3968 }
3965 3969
3966 3970 .comment-form textarea {
3967 3971 width: 100%;
3968 3972 height: 100px;
3969 3973 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
3970 3974 }
3971 3975
3972 3976 form.comment-form {
3973 3977 margin-top: 10px;
3974 3978 margin-left: 10px;
3975 3979 }
3976 3980
3977 3981 .comment-form-submit {
3978 3982 margin-top: 5px;
3979 3983 margin-left: 525px;
3980 3984 }
3981 3985
3982 3986 .file-comments {
3983 3987 display: none;
3984 3988 }
3985 3989
3986 3990 .comment-form .comment {
3987 3991 margin-left: 10px;
3988 3992 }
3989 3993
3990 3994 .comment-form .comment-help{
3991 3995 padding: 0px 0px 5px 0px;
3992 3996 color: #666;
3993 3997 }
3994 3998
3995 3999 .comment-form .comment-button{
3996 4000 padding-top:5px;
3997 4001 }
3998 4002
3999 4003 .add-another-button {
4000 4004 margin-left: 10px;
4001 4005 margin-top: 10px;
4002 4006 margin-bottom: 10px;
4003 4007 }
4004 4008
4005 4009 .comment .buttons {
4006 4010 float: right;
4007 4011 padding:2px 2px 0px 0px;
4008 4012 }
4009 4013
4010 4014
4011 4015 .show-inline-comments{
4012 4016 position: relative;
4013 4017 top:1px
4014 4018 }
4015 4019
4016 4020 /** comment inline form **/
4017 4021 .comment-inline-form .overlay{
4018 4022 display: none;
4019 4023 }
4020 4024 .comment-inline-form .overlay.submitting{
4021 4025 display:block;
4022 4026 background: none repeat scroll 0 0 white;
4023 4027 font-size: 16px;
4024 4028 opacity: 0.5;
4025 4029 position: absolute;
4026 4030 text-align: center;
4027 4031 vertical-align: top;
4028 4032
4029 4033 }
4030 4034 .comment-inline-form .overlay.submitting .overlay-text{
4031 4035 width:100%;
4032 4036 margin-top:5%;
4033 4037 }
4034 4038
4035 4039 .comment-inline-form .clearfix{
4036 4040 background: #EEE;
4037 4041 -webkit-border-radius: 4px;
4038 4042 -moz-border-radius: 4px;
4039 4043 border-radius: 4px;
4040 4044 padding: 5px;
4041 4045 }
4042 4046
4043 4047 div.comment-inline-form {
4044 4048 margin-top: 5px;
4045 4049 padding:2px 6px 8px 6px;
4046 4050
4047 4051 }
4048 4052
4049 4053 .comment-inline-form strong {
4050 4054 display: block;
4051 4055 margin-bottom: 15px;
4052 4056 }
4053 4057
4054 4058 .comment-inline-form textarea {
4055 4059 width: 100%;
4056 4060 height: 100px;
4057 4061 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
4058 4062 }
4059 4063
4060 4064 form.comment-inline-form {
4061 4065 margin-top: 10px;
4062 4066 margin-left: 10px;
4063 4067 }
4064 4068
4065 4069 .comment-inline-form-submit {
4066 4070 margin-top: 5px;
4067 4071 margin-left: 525px;
4068 4072 }
4069 4073
4070 4074 .file-comments {
4071 4075 display: none;
4072 4076 }
4073 4077
4074 4078 .comment-inline-form .comment {
4075 4079 margin-left: 10px;
4076 4080 }
4077 4081
4078 4082 .comment-inline-form .comment-help{
4079 4083 padding: 0px 0px 2px 0px;
4080 4084 color: #666666;
4081 4085 font-size: 10px;
4082 4086 }
4083 4087
4084 4088 .comment-inline-form .comment-button{
4085 4089 padding-top:5px;
4086 4090 }
4087 4091
4088 4092 /** comment inline **/
4089 4093 .inline-comments {
4090 4094 padding:10px 20px;
4091 4095 }
4092 4096
4093 4097 .inline-comments div.rst-block {
4094 4098 clear:both;
4095 4099 overflow:hidden;
4096 4100 margin:0;
4097 4101 padding:0 20px 0px;
4098 4102 }
4099 4103 .inline-comments .comment {
4100 4104 border: 1px solid #ddd;
4101 4105 -webkit-border-radius: 4px;
4102 4106 -moz-border-radius: 4px;
4103 4107 border-radius: 4px;
4104 4108 margin: 3px 3px 5px 5px;
4105 4109 background-color: #FAFAFA;
4106 4110 }
4107 4111 .inline-comments .add-comment {
4108 4112 padding: 2px 4px 8px 5px;
4109 4113 }
4110 4114
4111 4115 .inline-comments .comment-wrapp{
4112 4116 padding:1px;
4113 4117 }
4114 4118 .inline-comments .comment .meta {
4115 4119 background: #f8f8f8;
4116 4120 padding: 4px;
4117 4121 border-bottom: 1px solid #ddd;
4118 4122 }
4119 4123
4120 4124 .inline-comments .comment .meta img {
4121 4125 vertical-align: middle;
4122 4126 }
4123 4127
4124 4128 .inline-comments .comment .meta .user {
4125 4129 font-weight: bold;
4126 4130 }
4127 4131
4128 4132 .inline-comments .comment .meta .date {
4129 4133 }
4130 4134
4131 4135 .inline-comments .comment .text {
4132 4136 background-color: #FAFAFA;
4133 4137 }
4134 4138
4135 4139 .inline-comments .comments-number{
4136 4140 padding:0px 0px 10px 0px;
4137 4141 font-weight: bold;
4138 4142 color: #666;
4139 4143 font-size: 16px;
4140 4144 }
4141 4145 .inline-comments-button .add-comment{
4142 4146 margin:2px 0px 8px 5px !important
4143 4147 }
4144 4148
4145 4149
4146 4150 .notification-paginator{
4147 4151 padding: 0px 0px 4px 16px;
4148 4152 float: left;
4149 4153 }
4150 4154
4151 4155 .notifications{
4152 4156 border-radius: 4px 4px 4px 4px;
4153 4157 -webkit-border-radius: 4px;
4154 4158 -moz-border-radius: 4px;
4155 4159 float: right;
4156 4160 margin: 20px 0px 0px 0px;
4157 4161 position: absolute;
4158 4162 text-align: center;
4159 4163 width: 26px;
4160 4164 z-index: 1000;
4161 4165 }
4162 4166 .notifications a{
4163 4167 color:#888 !important;
4164 4168 display: block;
4165 4169 font-size: 10px;
4166 4170 background-color: #DEDEDE !important;
4167 4171 border-radius: 2px !important;
4168 4172 -webkit-border-radius: 2px !important;
4169 4173 -moz-border-radius: 2px !important;
4170 4174 }
4171 4175 .notifications a:hover{
4172 4176 text-decoration: none !important;
4173 4177 background-color: #EEEFFF !important;
4174 4178 }
4175 4179 .notification-header{
4176 4180 padding-top:6px;
4177 4181 }
4178 4182 .notification-header .desc{
4179 4183 font-size: 16px;
4180 4184 height: 24px;
4181 4185 float: left
4182 4186 }
4183 4187 .notification-list .container.unread{
4184 4188 background: none repeat scroll 0 0 rgba(255, 255, 180, 0.6);
4185 4189 }
4186 4190 .notification-header .gravatar{
4187 4191 background: none repeat scroll 0 0 transparent;
4188 4192 padding: 0px 0px 0px 8px;
4189 4193 }
4190 4194 .notification-header .desc.unread{
4191 4195 font-weight: bold;
4192 4196 font-size: 17px;
4193 4197 }
4194 4198 .notification-table{
4195 4199 border: 1px solid #ccc;
4196 4200 -webkit-border-radius: 6px 6px 6px 6px;
4197 4201 -moz-border-radius: 6px 6px 6px 6px;
4198 4202 border-radius: 6px 6px 6px 6px;
4199 4203 clear: both;
4200 4204 margin: 0px 20px 0px 20px;
4201 4205 }
4202 4206 .notification-header .delete-notifications{
4203 4207 float: right;
4204 4208 padding-top: 8px;
4205 4209 cursor: pointer;
4206 4210 }
4207 4211 .notification-subject{
4208 4212 clear:both;
4209 4213 border-bottom: 1px solid #eee;
4210 4214 padding:5px 0px 5px 38px;
4211 4215 }
4212 4216
4213 4217 .notification-body{
4214 4218 clear:both;
4215 4219 margin: 34px 2px 2px 8px
4216 4220 }
4217 4221
4218 4222 /****
4219 4223 PERMS
4220 4224 *****/
4221 4225 #perms .perms_section_head {
4222 4226 padding:10px 10px 10px 0px;
4223 4227 font-size:16px;
4224 4228 font-weight: bold;
4225 4229 }
4226 4230
4227 4231 #perms .perm_tag{
4228 4232 padding: 1px 3px 1px 3px;
4229 4233 font-size: 10px;
4230 4234 font-weight: bold;
4231 4235 text-transform: uppercase;
4232 4236 white-space: nowrap;
4233 4237 -webkit-border-radius: 3px;
4234 4238 -moz-border-radius: 3px;
4235 4239 border-radius: 3px;
4236 4240 }
4237 4241
4238 4242 #perms .perm_tag.admin{
4239 4243 background-color: #B94A48;
4240 4244 color: #ffffff;
4241 4245 }
4242 4246
4243 4247 #perms .perm_tag.write{
4244 4248 background-color: #B94A48;
4245 4249 color: #ffffff;
4246 4250 }
4247 4251
4248 4252 #perms .perm_tag.read{
4249 4253 background-color: #468847;
4250 4254 color: #ffffff;
4251 4255 }
4252 4256
4253 4257 #perms .perm_tag.none{
4254 4258 background-color: #bfbfbf;
4255 4259 color: #ffffff;
4256 4260 }
4257 4261
4258 4262 .perm-gravatar{
4259 4263 vertical-align:middle;
4260 4264 padding:2px;
4261 4265 }
4262 4266 .perm-gravatar-ac{
4263 4267 vertical-align:middle;
4264 4268 padding:2px;
4265 4269 width: 14px;
4266 4270 height: 14px;
4267 4271 }
4268 4272
4269 4273 /*****************************************************************************
4270 4274 DIFFS CSS
4271 4275 ******************************************************************************/
4272 4276
4273 4277 div.diffblock {
4274 4278 overflow: auto;
4275 4279 padding: 0px;
4276 4280 border: 1px solid #ccc;
4277 4281 background: #f8f8f8;
4278 4282 font-size: 100%;
4279 4283 line-height: 100%;
4280 4284 /* new */
4281 4285 line-height: 125%;
4282 4286 -webkit-border-radius: 6px 6px 0px 0px;
4283 4287 -moz-border-radius: 6px 6px 0px 0px;
4284 4288 border-radius: 6px 6px 0px 0px;
4285 4289 }
4286 4290 div.diffblock.margined{
4287 4291 margin: 0px 20px 0px 20px;
4288 4292 }
4289 4293 div.diffblock .code-header{
4290 4294 border-bottom: 1px solid #CCCCCC;
4291 4295 background: #EEEEEE;
4292 4296 padding:10px 0 10px 0;
4293 4297 height: 14px;
4294 4298 }
4295 4299 div.diffblock .code-header.cv{
4296 4300 height: 34px;
4297 4301 }
4298 4302 div.diffblock .code-header-title{
4299 4303 padding: 0px 0px 10px 5px !important;
4300 4304 margin: 0 !important;
4301 4305 }
4302 4306 div.diffblock .code-header .hash{
4303 4307 float: left;
4304 4308 padding: 2px 0 0 2px;
4305 4309 }
4306 4310 div.diffblock .code-header .date{
4307 4311 float:left;
4308 4312 text-transform: uppercase;
4309 4313 padding: 2px 0px 0px 2px;
4310 4314 }
4311 4315 div.diffblock .code-header div{
4312 4316 margin-left:4px;
4313 4317 font-weight: bold;
4314 4318 font-size: 14px;
4315 4319 }
4316 4320 div.diffblock .code-body{
4317 4321 background: #FFFFFF;
4318 4322 }
4319 4323 div.diffblock pre.raw{
4320 4324 background: #FFFFFF;
4321 4325 color:#000000;
4322 4326 }
4323 4327 table.code-difftable{
4324 4328 border-collapse: collapse;
4325 4329 width: 99%;
4326 4330 }
4327 4331 table.code-difftable td {
4328 4332 padding: 0 !important;
4329 4333 background: none !important;
4330 4334 border:0 !important;
4331 4335 vertical-align: none !important;
4332 4336 }
4333 4337 table.code-difftable .context{
4334 4338 background:none repeat scroll 0 0 #DDE7EF;
4335 4339 }
4336 4340 table.code-difftable .add{
4337 4341 background:none repeat scroll 0 0 #DDFFDD;
4338 4342 }
4339 4343 table.code-difftable .add ins{
4340 4344 background:none repeat scroll 0 0 #AAFFAA;
4341 4345 text-decoration:none;
4342 4346 }
4343 4347 table.code-difftable .del{
4344 4348 background:none repeat scroll 0 0 #FFDDDD;
4345 4349 }
4346 4350 table.code-difftable .del del{
4347 4351 background:none repeat scroll 0 0 #FFAAAA;
4348 4352 text-decoration:none;
4349 4353 }
4350 4354
4351 4355 /** LINE NUMBERS **/
4352 4356 table.code-difftable .lineno{
4353 4357
4354 4358 padding-left:2px;
4355 4359 padding-right:2px;
4356 4360 text-align:right;
4357 4361 width:32px;
4358 4362 -moz-user-select:none;
4359 4363 -webkit-user-select: none;
4360 4364 border-right: 1px solid #CCC !important;
4361 4365 border-left: 0px solid #CCC !important;
4362 4366 border-top: 0px solid #CCC !important;
4363 4367 border-bottom: none !important;
4364 4368 vertical-align: middle !important;
4365 4369
4366 4370 }
4367 4371 table.code-difftable .lineno.new {
4368 4372 }
4369 4373 table.code-difftable .lineno.old {
4370 4374 }
4371 4375 table.code-difftable .lineno a{
4372 4376 color:#747474 !important;
4373 4377 font:11px "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace !important;
4374 4378 letter-spacing:-1px;
4375 4379 text-align:right;
4376 4380 padding-right: 2px;
4377 4381 cursor: pointer;
4378 4382 display: block;
4379 4383 width: 32px;
4380 4384 }
4381 4385
4382 4386 table.code-difftable .lineno-inline{
4383 4387 background:none repeat scroll 0 0 #FFF !important;
4384 4388 padding-left:2px;
4385 4389 padding-right:2px;
4386 4390 text-align:right;
4387 4391 width:30px;
4388 4392 -moz-user-select:none;
4389 4393 -webkit-user-select: none;
4390 4394 }
4391 4395
4392 4396 /** CODE **/
4393 4397 table.code-difftable .code {
4394 4398 display: block;
4395 4399 width: 100%;
4396 4400 }
4397 4401 table.code-difftable .code td{
4398 4402 margin:0;
4399 4403 padding:0;
4400 4404 }
4401 4405 table.code-difftable .code pre{
4402 4406 margin:0;
4403 4407 padding:0;
4404 4408 height: 17px;
4405 4409 line-height: 17px;
4406 4410 }
4407 4411
4408 4412
4409 4413 .diffblock.margined.comm .line .code:hover{
4410 4414 background-color:#FFFFCC !important;
4411 4415 cursor: pointer !important;
4412 4416 background-image:url("../images/icons/comment_add.png") !important;
4413 4417 background-repeat:no-repeat !important;
4414 4418 background-position: right !important;
4415 4419 background-position: 0% 50% !important;
4416 4420 }
4417 4421 .diffblock.margined.comm .line .code.no-comment:hover{
4418 4422 background-image: none !important;
4419 4423 cursor: auto !important;
4420 4424 background-color: inherit !important;
4421 4425
4422 4426 }
@@ -1,237 +1,245 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="info_box" style="clear: both;padding: 10px 6px;vertical-align: right;text-align: right;"><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="ui-btn small">${_('Open new pull request')}</a></div>
35 35 <div class="container_header">
36 36 ${h.form(h.url.current(),method='get')}
37 37 <div class="info_box" style="float:left">
38 38 ${h.submit('set',_('Show'),class_="ui-btn")}
39 39 ${h.text('size',size=1,value=c.size)}
40 40 ${_('revisions')}
41 41 </div>
42 42 ${h.end_form()}
43 43 <div id="rev_range_container" style="display:none"></div>
44 44 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
45 45 </div>
46 46
47 47 %for cnt,cs in enumerate(c.pagination):
48 48 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
49 49 <div class="left">
50 50 <div>
51 51 ${h.checkbox(cs.short_id,class_="changeset_range")}
52 52 <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>
53 53 </div>
54 54 <div class="author">
55 55 <div class="gravatar">
56 56 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
57 57 </div>
58 58 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
59 59 </div>
60 60 <div class="date">${cs.date}</div>
61 61 </div>
62 62 <div class="mid">
63 63 <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>
64 64 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
65 65 </div>
66 66 <div class="right">
67 67 <div class="changes">
68 68 <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>
69 69 <div class="comments-container">
70 70 %if len(c.comments.get(cs.raw_id,[])) > 0:
71 71 <div class="comments-cnt" title="${('comments')}">
72 72 <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)}">
73 73 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
74 74 <img src="${h.url('/images/icons/comments.png')}">
75 75 </a>
76 76 </div>
77 77 %endif
78 78 </div>
79 79 <div class="changeset-status-container">
80 80 %if c.statuses.get(cs.raw_id):
81 81 <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div>
82 82 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></div>
83 83 %endif
84 84 </div>
85 85 </div>
86 86 %if cs.parents:
87 87 %for p_cs in reversed(cs.parents):
88 88 <div class="parent">${_('Parent')}
89 89 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
90 90 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
91 91 </div>
92 92 %endfor
93 93 %else:
94 94 <div class="parent">${_('No parents')}</div>
95 95 %endif
96 96
97 97 <span class="logtags">
98 98 %if len(cs.parents)>1:
99 99 <span class="merge">${_('merge')}</span>
100 100 %endif
101 101 %if cs.branch:
102 102 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
103 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
103 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
104 </span>
104 105 %endif
106 %if h.is_hg(c.rhodecode_repo):
107 %for book in cs.bookmarks:
108 <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
109 ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
110 </span>
111 %endfor
112 %endif
105 113 %for tag in cs.tags:
106 114 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
107 115 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
108 116 %endfor
109 117 </span>
110 118 </div>
111 119 </div>
112 120
113 121 %endfor
114 122 <div class="pagination-wh pagination-left">
115 123 ${c.pagination.pager('$link_previous ~2~ $link_next')}
116 124 </div>
117 125 </div>
118 126 </div>
119 127
120 128 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
121 129 <script type="text/javascript">
122 130 YAHOO.util.Event.onDOMReady(function(){
123 131
124 132 //Monitor range checkboxes and build a link to changesets
125 133 //ranges
126 134 var checkboxes = YUD.getElementsByClassName('changeset_range');
127 135 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
128 136 YUE.on(checkboxes,'click',function(e){
129 137 var checked_checkboxes = [];
130 138 for (pos in checkboxes){
131 139 if(checkboxes[pos].checked){
132 140 checked_checkboxes.push(checkboxes[pos]);
133 141 }
134 142 }
135 143 if(checked_checkboxes.length>1){
136 144 var rev_end = checked_checkboxes[0].name;
137 145 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
138 146
139 147 var url = url_tmpl.replace('__REVRANGE__',
140 148 rev_start+'...'+rev_end);
141 149
142 150 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
143 151 link = link.replace('__S',rev_start);
144 152 link = link.replace('__E',rev_end);
145 153 YUD.get('rev_range_container').innerHTML = link;
146 154 YUD.setStyle('rev_range_container','display','');
147 155 }
148 156 else{
149 157 YUD.setStyle('rev_range_container','display','none');
150 158
151 159 }
152 160 });
153 161
154 162 var msgs = YUQ('.message');
155 163 // get first element height
156 164 var el = YUQ('#graph_content .container')[0];
157 165 var row_h = el.clientHeight;
158 166 for(var i=0;i<msgs.length;i++){
159 167 var m = msgs[i];
160 168
161 169 var h = m.clientHeight;
162 170 var pad = YUD.getStyle(m,'padding');
163 171 if(h > row_h){
164 172 var offset = row_h - (h+12);
165 173 YUD.setStyle(m.nextElementSibling,'display','block');
166 174 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
167 175 };
168 176 }
169 177 YUE.on(YUQ('.expand'),'click',function(e){
170 178 var elem = e.currentTarget.parentNode.parentNode;
171 179 YUD.setStyle(e.currentTarget,'display','none');
172 180 YUD.setStyle(elem,'height','auto');
173 181
174 182 //redraw the graph, max_w and jsdata are global vars
175 183 set_canvas(max_w);
176 184
177 185 var r = new BranchRenderer();
178 186 r.render(jsdata,max_w);
179 187
180 188 })
181 189
182 190 // Fetch changeset details
183 191 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
184 192 var id = e.currentTarget.id
185 193 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
186 194 var url = url.replace('__CS__',id);
187 195 ypjax(url,id,function(){tooltip_activate()});
188 196 });
189 197
190 198 // change branch filter
191 199 YUE.on(YUD.get('branch_filter'),'change',function(e){
192 200 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
193 201 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
194 202 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
195 203 var url = url.replace('__BRANCH__',selected_branch);
196 204 if(selected_branch != ''){
197 205 window.location = url;
198 206 }else{
199 207 window.location = url_main;
200 208 }
201 209
202 210 });
203 211
204 212 function set_canvas(heads) {
205 213 var c = document.getElementById('graph_nodes');
206 214 var t = document.getElementById('graph_content');
207 215 canvas = document.getElementById('graph_canvas');
208 216 var div_h = t.clientHeight;
209 217 c.style.height=div_h+'px';
210 218 canvas.setAttribute('height',div_h);
211 219 c.style.height=max_w+'px';
212 220 canvas.setAttribute('width',max_w);
213 221 };
214 222 var heads = 1;
215 223 var max_heads = 0;
216 224 var jsdata = ${c.jsdata|n};
217 225
218 226 for( var i=0;i<jsdata.length;i++){
219 227 var m = Math.max.apply(Math, jsdata[i][1]);
220 228 if (m>max_heads){
221 229 max_heads = m;
222 230 }
223 231 }
224 232 var max_w = Math.max(100,max_heads*25);
225 233 set_canvas(max_w);
226 234
227 235 var r = new BranchRenderer();
228 236 r.render(jsdata,max_w);
229 237
230 238 });
231 239 </script>
232 240 %else:
233 241 ${_('There are no changes yet')}
234 242 %endif
235 243 </div>
236 244 </div>
237 245 </%def>
@@ -1,109 +1,112 b''
1 1 """Pylons application test package
2 2
3 3 This package assumes the Pylons environment is already loaded, such as
4 4 when this script is imported from the `nosetests --with-pylons=test.ini`
5 5 command.
6 6
7 7 This module initializes the application via ``websetup`` (`paster
8 8 setup-app`) and provides the base testing objects.
9 9 """
10 10 import os
11 11 import time
12 12 import logging
13 13 from os.path import join as jn
14 14
15 15 from unittest import TestCase
16 16 from tempfile import _RandomNameSequence
17 17
18 18 from paste.deploy import loadapp
19 19 from paste.script.appinstall import SetupCommand
20 20 from pylons import config, url
21 21 from routes.util import URLGenerator
22 22 from webtest import TestApp
23 23
24 from rhodecode import is_windows
24 25 from rhodecode.model.meta import Session
25 26 from rhodecode.model.db import User
26 27
27 28 import pylons.test
28 29
29 30 os.environ['TZ'] = 'UTC'
30 time.tzset()
31 if not is_windows:
32 time.tzset()
31 33
32 34 log = logging.getLogger(__name__)
33 35
34 36 __all__ = [
35 37 'environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO',
36 38 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK',
37 39 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
38 40 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
39 41 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL'
40 42 ]
41 43
42 44 # Invoke websetup with the current config file
43 45 # SetupCommand('setup-app').run([config_file])
44 46
45 47 ##RUNNING DESIRED TESTS
46 48 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
47 49 # nosetests --pdb --pdb-failures
48 50 environ = {}
49 51
50 52 #SOME GLOBALS FOR TESTS
51 53
52 54 TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
53 55 TEST_USER_ADMIN_LOGIN = 'test_admin'
54 56 TEST_USER_ADMIN_PASS = 'test12'
55 57 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
56 58
57 59 TEST_USER_REGULAR_LOGIN = 'test_regular'
58 60 TEST_USER_REGULAR_PASS = 'test12'
59 61 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
60 62
61 63 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
62 64 TEST_USER_REGULAR2_PASS = 'test12'
63 65 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
64 66
65 67 HG_REPO = 'vcs_test_hg'
66 68 GIT_REPO = 'vcs_test_git'
67 69
68 70 NEW_HG_REPO = 'vcs_test_hg_new'
69 71 NEW_GIT_REPO = 'vcs_test_git_new'
70 72
71 73 HG_FORK = 'vcs_test_hg_fork'
72 74 GIT_FORK = 'vcs_test_git_fork'
73 75
76
74 77 class TestController(TestCase):
75 78
76 79 def __init__(self, *args, **kwargs):
77 80 wsgiapp = pylons.test.pylonsapp
78 81 config = wsgiapp.config
79 82
80 83 self.app = TestApp(wsgiapp)
81 84 url._push_object(URLGenerator(config['routes.map'], environ))
82 85 self.Session = Session
83 86 self.index_location = config['app_conf']['index_dir']
84 87 TestCase.__init__(self, *args, **kwargs)
85 88
86 89 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
87 90 password=TEST_USER_ADMIN_PASS):
88 91 self._logged_username = username
89 92 response = self.app.post(url(controller='login', action='index'),
90 93 {'username':username,
91 94 'password':password})
92 95
93 96 if 'invalid user name' in response.body:
94 97 self.fail('could not login using %s %s' % (username, password))
95 98
96 99 self.assertEqual(response.status, '302 Found')
97 100 ses = response.session['rhodecode_user']
98 101 self.assertEqual(ses.get('username'), username)
99 102 response = response.follow()
100 103 self.assertEqual(ses.get('is_authenticated'), True)
101 104
102 105 return response.session['rhodecode_user']
103 106
104 107 def _get_logged_user(self):
105 108 return User.get_by_username(self._logged_username)
106 109
107 110 def checkSessionFlash(self, response, msg):
108 111 self.assertTrue('flash' in response.session)
109 112 self.assertTrue(msg in response.session['flash'][0][1])
@@ -1,315 +1,318 b''
1 1 from rhodecode.tests import *
2 2
3 3 ARCHIVE_SPECS = {
4 4 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
5 5 '.tar.gz': ('application/x-gzip', 'tgz', ''),
6 6 '.zip': ('application/zip', 'zip', ''),
7 7 }
8 8
9 9
10 10 class TestFilesController(TestController):
11 11
12 12 def test_index(self):
13 13 self.log_user()
14 14 response = self.app.get(url(controller='files', action='index',
15 15 repo_name=HG_REPO,
16 16 revision='tip',
17 17 f_path='/'))
18 18 # Test response...
19 19 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
20 20 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
21 21 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
22 22 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
23 23 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
24 24
25 25 def test_index_revision(self):
26 26 self.log_user()
27 27
28 28 response = self.app.get(
29 29 url(controller='files', action='index',
30 30 repo_name=HG_REPO,
31 31 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
32 32 f_path='/')
33 33 )
34 34
35 35 #Test response...
36 36
37 37 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
38 38 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
39 39 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
40 40 response.mustcontain('1.1 KiB')
41 41 response.mustcontain('text/x-python')
42 42
43 43 def test_index_different_branch(self):
44 44 self.log_user()
45 45
46 46 response = self.app.get(url(controller='files', action='index',
47 47 repo_name=HG_REPO,
48 48 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
49 49 f_path='/'))
50 50
51 51 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
52 52
53 53 def test_index_paging(self):
54 54 self.log_user()
55 55
56 56 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
57 57 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
58 58 (109, '75feb4c33e81186c87eac740cee2447330288412'),
59 59 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
60 60 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
61 61
62 62 response = self.app.get(url(controller='files', action='index',
63 63 repo_name=HG_REPO,
64 64 revision=r[1],
65 65 f_path='/'))
66 66
67 67 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
68 68
69 69 def test_file_source(self):
70 70 self.log_user()
71 71 response = self.app.get(url(controller='files', action='index',
72 72 repo_name=HG_REPO,
73 73 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
74 74 f_path='vcs/nodes.py'))
75 75
76 76 #test or history
77 77 response.mustcontain("""<optgroup label="Changesets">
78 78 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
79 79 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
80 80 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
81 81 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
82 82 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
83 83 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
84 84 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
85 85 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
86 86 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
87 87 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
88 88 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
89 89 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
90 90 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
91 91 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
92 92 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
93 93 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
94 94 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
95 95 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
96 96 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
97 97 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
98 98 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
99 99 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
100 100 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
101 101 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
102 102 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
103 103 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
104 104 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
105 105 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
106 106 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
107 107 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
108 108 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
109 109 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
110 110 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
111 111 </optgroup>
112 112 <optgroup label="Branches">
113 113 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
114 114 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
115 115 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
116 116 </optgroup>
117 117 <optgroup label="Tags">
118 118 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
119 119 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
120 120 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
121 121 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
122 122 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
123 123 </optgroup>
124 124 """)
125 125
126 126 response.mustcontain("""<div class="commit">merge</div>""")
127 127
128 128 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
129 129
130 130 def test_file_annotation(self):
131 131 self.log_user()
132 132 response = self.app.get(url(controller='files', action='index',
133 133 repo_name=HG_REPO,
134 134 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
135 135 f_path='vcs/nodes.py',
136 136 annotate=True))
137 137
138 138
139 139 response.mustcontain("""<optgroup label="Changesets">
140 140 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
141 141 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
142 142 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
143 143 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
144 144 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
145 145 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
146 146 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
147 147 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
148 148 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
149 149 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
150 150 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
151 151 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
152 152 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
153 153 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
154 154 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
155 155 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
156 156 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
157 157 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
158 158 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
159 159 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
160 160 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
161 161 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
162 162 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
163 163 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
164 164 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
165 165 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
166 166 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
167 167 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
168 168 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
169 169 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
170 170 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
171 171 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
172 172 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
173 173 </optgroup>
174 174 <optgroup label="Branches">
175 175 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
176 176 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
177 177 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
178 178 </optgroup>
179 179 <optgroup label="Tags">
180 180 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
181 181 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
182 182 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
183 183 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
184 184 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
185 185 </optgroup>""")
186 186
187 187 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
188 188
189 189 def test_archival(self):
190 190 self.log_user()
191 191
192 192 for arch_ext, info in ARCHIVE_SPECS.items():
193 short = '27cd5cce30c9%s' % arch_ext
193 194 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
194 filename = '%s-%s' % (HG_REPO, fname)
195
196 response = self.app.get(url(controller='files', action='archivefile',
195 filename = '%s-%s' % (HG_REPO, short)
196 response = self.app.get(url(controller='files',
197 action='archivefile',
197 198 repo_name=HG_REPO,
198 199 fname=fname))
199 200
200 assert response.status == '200 OK', 'wrong response code'
201 assert response.response._headers.items() == [('Pragma', 'no-cache'),
202 ('Cache-Control', 'no-cache'),
203 ('Content-Type', '%s; charset=utf-8' % info[0]),
204 ('Content-Disposition', 'attachment; filename=%s' % filename), ], 'wrong headers'
201 self.assertEqual(response.status, '200 OK')
202 self.assertEqual(response.response._headers.items(),
203 [('Pragma', 'no-cache'),
204 ('Cache-Control', 'no-cache'),
205 ('Content-Type', '%s; charset=utf-8' % info[0]),
206 ('Content-Disposition', 'attachment; filename=%s' % filename),
207 ]
208 )
205 209
206 210 def test_archival_wrong_ext(self):
207 211 self.log_user()
208 212
209 213 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
210 214 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
211 215
212 216 response = self.app.get(url(controller='files', action='archivefile',
213 217 repo_name=HG_REPO,
214 218 fname=fname))
215 assert 'Unknown archive type' in response.body
216
219 response.mustcontain('Unknown archive type')
217 220
218 221 def test_archival_wrong_revision(self):
219 222 self.log_user()
220 223
221 224 for rev in ['00x000000', 'tar', 'wrong', '@##$@$424213232', '232dffcd']:
222 225 fname = '%s.zip' % rev
223 226
224 227 response = self.app.get(url(controller='files', action='archivefile',
225 228 repo_name=HG_REPO,
226 229 fname=fname))
227 assert 'Unknown revision' in response.body
230 response.mustcontain('Unknown revision')
228 231
229 232 #==========================================================================
230 233 # RAW FILE
231 234 #==========================================================================
232 235 def test_raw_file_ok(self):
233 236 self.log_user()
234 237 response = self.app.get(url(controller='files', action='rawfile',
235 238 repo_name=HG_REPO,
236 239 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
237 240 f_path='vcs/nodes.py'))
238 241
239 assert response.content_disposition == "attachment; filename=nodes.py"
240 assert response.content_type == "text/x-python"
242 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
243 self.assertEqual(response.content_type, "text/x-python")
241 244
242 245 def test_raw_file_wrong_cs(self):
243 246 self.log_user()
244 247 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
245 248 f_path = 'vcs/nodes.py'
246 249
247 250 response = self.app.get(url(controller='files', action='rawfile',
248 251 repo_name=HG_REPO,
249 252 revision=rev,
250 253 f_path=f_path))
251 254
252 255 msg = """Revision %r does not exist for this repository""" % (rev)
253 256 self.checkSessionFlash(response, msg)
254 257
255 258 msg = """%s""" % (HG_REPO)
256 259 self.checkSessionFlash(response, msg)
257 260
258 261 def test_raw_file_wrong_f_path(self):
259 262 self.log_user()
260 263 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
261 264 f_path = 'vcs/ERRORnodes.py'
262 265 response = self.app.get(url(controller='files', action='rawfile',
263 266 repo_name=HG_REPO,
264 267 revision=rev,
265 268 f_path=f_path))
266 269
267 270 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
268 271 self.checkSessionFlash(response, msg)
269 272
270 273 #==========================================================================
271 274 # RAW RESPONSE - PLAIN
272 275 #==========================================================================
273 276 def test_raw_ok(self):
274 277 self.log_user()
275 278 response = self.app.get(url(controller='files', action='raw',
276 279 repo_name=HG_REPO,
277 280 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
278 281 f_path='vcs/nodes.py'))
279 282
280 assert response.content_type == "text/plain"
283 self.assertEqual(response.content_type, "text/plain")
281 284
282 285 def test_raw_wrong_cs(self):
283 286 self.log_user()
284 287 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
285 288 f_path = 'vcs/nodes.py'
286 289
287 290 response = self.app.get(url(controller='files', action='raw',
288 291 repo_name=HG_REPO,
289 292 revision=rev,
290 293 f_path=f_path))
291 294 msg = """Revision %r does not exist for this repository""" % (rev)
292 295 self.checkSessionFlash(response, msg)
293 296
294 297 msg = """%s""" % (HG_REPO)
295 298 self.checkSessionFlash(response, msg)
296 299
297 300 def test_raw_wrong_f_path(self):
298 301 self.log_user()
299 302 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
300 303 f_path = 'vcs/ERRORnodes.py'
301 304 response = self.app.get(url(controller='files', action='raw',
302 305 repo_name=HG_REPO,
303 306 revision=rev,
304 307 f_path=f_path))
305 308 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
306 309 self.checkSessionFlash(response, msg)
307 310
308 311 def test_ajaxed_files_list(self):
309 312 self.log_user()
310 313 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
311 314 response = self.app.get(
312 315 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
313 316 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
314 317 )
315 318 response.mustcontain("vcs/web/simplevcs/views/repository.py")
@@ -1,268 +1,267 b''
1 1 # -*- coding: utf-8 -*-
2 2 from rhodecode.tests import *
3 3 from rhodecode.model.db import User, Notification
4 4 from rhodecode.lib.utils2 import generate_api_key
5 5 from rhodecode.lib.auth import check_password
6 6 from rhodecode.model.meta import Session
7 7
8 8
9 9 class TestLoginController(TestController):
10 10
11 11 def tearDown(self):
12 12 for n in Notification.query().all():
13 13 Session.delete(n)
14 14
15 15 Session.commit()
16 16 self.assertEqual(Notification.query().all(), [])
17 17
18 18 def test_index(self):
19 19 response = self.app.get(url(controller='login', action='index'))
20 20 self.assertEqual(response.status, '200 OK')
21 21 # Test response...
22 22
23 23 def test_login_admin_ok(self):
24 24 response = self.app.post(url(controller='login', action='index'),
25 25 {'username':'test_admin',
26 26 'password':'test12'})
27 27 self.assertEqual(response.status, '302 Found')
28 28 self.assertEqual(response.session['rhodecode_user'].get('username') ,
29 29 'test_admin')
30 30 response = response.follow()
31 31 self.assertTrue('%s repository' % HG_REPO in response.body)
32 32
33 33 def test_login_regular_ok(self):
34 34 response = self.app.post(url(controller='login', action='index'),
35 35 {'username':'test_regular',
36 36 'password':'test12'})
37 37
38 38 self.assertEqual(response.status, '302 Found')
39 39 self.assertEqual(response.session['rhodecode_user'].get('username') ,
40 40 'test_regular')
41 41 response = response.follow()
42 42 self.assertTrue('%s repository' % HG_REPO in response.body)
43 43 self.assertTrue('<a title="Admin" href="/_admin">' not in response.body)
44 44
45 45 def test_login_ok_came_from(self):
46 46 test_came_from = '/_admin/users'
47 47 response = self.app.post(url(controller='login', action='index',
48 48 came_from=test_came_from),
49 49 {'username':'test_admin',
50 50 'password':'test12'})
51 51 self.assertEqual(response.status, '302 Found')
52 52 response = response.follow()
53 53
54 54 self.assertEqual(response.status, '200 OK')
55 55 self.assertTrue('Users administration' in response.body)
56 56
57
58 57 def test_login_short_password(self):
59 58 response = self.app.post(url(controller='login', action='index'),
60 59 {'username':'test_admin',
61 60 'password':'as'})
62 61 self.assertEqual(response.status, '200 OK')
63 62
64 63 self.assertTrue('Enter 3 characters or more' in response.body)
65 64
66 65 def test_login_wrong_username_password(self):
67 66 response = self.app.post(url(controller='login', action='index'),
68 67 {'username':'error',
69 68 'password':'test12'})
70 69 self.assertEqual(response.status , '200 OK')
71 70
72 71 self.assertTrue('invalid user name' in response.body)
73 72 self.assertTrue('invalid password' in response.body)
74 73
75 74 #==========================================================================
76 75 # REGISTRATIONS
77 76 #==========================================================================
78 77 def test_register(self):
79 78 response = self.app.get(url(controller='login', action='register'))
80 79 self.assertTrue('Sign Up to RhodeCode' in response.body)
81 80
82 81 def test_register_err_same_username(self):
83 82 response = self.app.post(url(controller='login', action='register'),
84 83 {'username':'test_admin',
85 84 'password':'test12',
86 85 'password_confirmation':'test12',
87 86 'email':'goodmail@domain.com',
88 87 'name':'test',
89 88 'lastname':'test'})
90 89
91 90 self.assertEqual(response.status , '200 OK')
92 91 self.assertTrue('This username already exists' in response.body)
93 92
94 93 def test_register_err_same_email(self):
95 94 response = self.app.post(url(controller='login', action='register'),
96 95 {'username':'test_admin_0',
97 96 'password':'test12',
98 97 'password_confirmation':'test12',
99 98 'email':'test_admin@mail.com',
100 99 'name':'test',
101 100 'lastname':'test'})
102 101
103 102 self.assertEqual(response.status , '200 OK')
104 assert 'This e-mail address is already taken' in response.body
103 response.mustcontain('This e-mail address is already taken')
105 104
106 105 def test_register_err_same_email_case_sensitive(self):
107 106 response = self.app.post(url(controller='login', action='register'),
108 107 {'username':'test_admin_1',
109 108 'password':'test12',
110 109 'password_confirmation':'test12',
111 110 'email':'TesT_Admin@mail.COM',
112 111 'name':'test',
113 112 'lastname':'test'})
114 113 self.assertEqual(response.status , '200 OK')
115 assert 'This e-mail address is already taken' in response.body
114 response.mustcontain('This e-mail address is already taken')
116 115
117 116 def test_register_err_wrong_data(self):
118 117 response = self.app.post(url(controller='login', action='register'),
119 118 {'username':'xs',
120 119 'password':'test',
121 120 'password_confirmation':'test',
122 121 'email':'goodmailm',
123 122 'name':'test',
124 123 'lastname':'test'})
125 124 self.assertEqual(response.status , '200 OK')
126 assert 'An email address must contain a single @' in response.body
127 assert 'Enter a value 6 characters long or more' in response.body
128
125 response.mustcontain('An email address must contain a single @')
126 response.mustcontain('Enter a value 6 characters long or more')
129 127
130 128 def test_register_err_username(self):
131 129 response = self.app.post(url(controller='login', action='register'),
132 130 {'username':'error user',
133 131 'password':'test12',
134 132 'password_confirmation':'test12',
135 133 'email':'goodmailm',
136 134 'name':'test',
137 135 'lastname':'test'})
138 136
139 137 self.assertEqual(response.status , '200 OK')
140 assert 'An email address must contain a single @' in response.body
141 assert ('Username may only contain '
138 response.mustcontain('An email address must contain a single @')
139 response.mustcontain('Username may only contain '
142 140 'alphanumeric characters underscores, '
143 141 'periods or dashes and must begin with '
144 'alphanumeric character') in response.body
142 'alphanumeric character')
145 143
146 144 def test_register_err_case_sensitive(self):
147 145 response = self.app.post(url(controller='login', action='register'),
148 146 {'username':'Test_Admin',
149 147 'password':'test12',
150 148 'password_confirmation':'test12',
151 149 'email':'goodmailm',
152 150 'name':'test',
153 151 'lastname':'test'})
154 152
155 153 self.assertEqual(response.status , '200 OK')
156 154 self.assertTrue('An email address must contain a single @' in response.body)
157 155 self.assertTrue('This username already exists' in response.body)
158 156
159
160
161 157 def test_register_special_chars(self):
162 158 response = self.app.post(url(controller='login', action='register'),
163 159 {'username':'xxxaxn',
164 160 'password':'ąćźżąśśśś',
165 161 'password_confirmation':'ąćźżąśśśś',
166 162 'email':'goodmailm@test.plx',
167 163 'name':'test',
168 164 'lastname':'test'})
169 165
170 166 self.assertEqual(response.status , '200 OK')
171 167 self.assertTrue('Invalid characters in password' in response.body)
172 168
173
174 169 def test_register_password_mismatch(self):
175 170 response = self.app.post(url(controller='login', action='register'),
176 171 {'username':'xs',
177 172 'password':'123qwe',
178 173 'password_confirmation':'qwe123',
179 174 'email':'goodmailm@test.plxa',
180 175 'name':'test',
181 176 'lastname':'test'})
182 177
183 self.assertEqual(response.status , '200 OK')
184 assert 'Passwords do not match' in response.body
178 self.assertEqual(response.status, '200 OK')
179 response.mustcontain('Passwords do not match')
185 180
186 181 def test_register_ok(self):
187 182 username = 'test_regular4'
188 183 password = 'qweqwe'
189 184 email = 'marcin@test.com'
190 185 name = 'testname'
191 186 lastname = 'testlastname'
192 187
193 188 response = self.app.post(url(controller='login', action='register'),
194 189 {'username':username,
195 190 'password':password,
196 191 'password_confirmation':password,
197 192 'email':email,
198 193 'name':name,
199 'lastname':lastname})
200 self.assertEqual(response.status , '302 Found')
201 assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration'
194 'lastname':lastname,
195 'admin':True}) # This should be overriden
196 self.assertEqual(response.status, '302 Found')
197 self.checkSessionFlash(response, 'You have successfully registered into rhodecode')
202 198
203 199 ret = self.Session.query(User).filter(User.username == 'test_regular4').one()
204 assert ret.username == username , 'field mismatch %s %s' % (ret.username, username)
205 assert check_password(password, ret.password) == True , 'password mismatch'
206 assert ret.email == email , 'field mismatch %s %s' % (ret.email, email)
207 assert ret.name == name , 'field mismatch %s %s' % (ret.name, name)
208 assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname)
209
200 self.assertEqual(ret.username, username)
201 self.assertEqual(check_password(password, ret.password), True)
202 self.assertEqual(ret.email, email)
203 self.assertEqual(ret.name, name)
204 self.assertEqual(ret.lastname, lastname)
205 self.assertNotEqual(ret.api_key, None)
206 self.assertEqual(ret.admin, False)
210 207
211 208 def test_forgot_password_wrong_mail(self):
212 response = self.app.post(url(controller='login', action='password_reset'),
213 {'email':'marcin@wrongmail.org', })
209 response = self.app.post(
210 url(controller='login', action='password_reset'),
211 {'email': 'marcin@wrongmail.org',}
212 )
214 213
215 assert "This e-mail address doesn't exist" in response.body, 'Missing error message about wrong email'
214 response.mustcontain("This e-mail address doesn't exist")
216 215
217 216 def test_forgot_password(self):
218 217 response = self.app.get(url(controller='login',
219 218 action='password_reset'))
220 self.assertEqual(response.status , '200 OK')
219 self.assertEqual(response.status, '200 OK')
221 220
222 221 username = 'test_password_reset_1'
223 222 password = 'qweqwe'
224 223 email = 'marcin@python-works.com'
225 224 name = 'passwd'
226 225 lastname = 'reset'
227 226
228 227 new = User()
229 228 new.username = username
230 229 new.password = password
231 230 new.email = email
232 231 new.name = name
233 232 new.lastname = lastname
234 233 new.api_key = generate_api_key(username)
235 234 self.Session.add(new)
236 235 self.Session.commit()
237 236
238 237 response = self.app.post(url(controller='login',
239 238 action='password_reset'),
240 239 {'email':email, })
241 240
242 241 self.checkSessionFlash(response, 'Your password reset link was sent')
243 242
244 243 response = response.follow()
245 244
246 245 # BAD KEY
247 246
248 247 key = "bad"
249 248 response = self.app.get(url(controller='login',
250 249 action='password_reset_confirmation',
251 250 key=key))
252 251 self.assertEqual(response.status, '302 Found')
253 252 self.assertTrue(response.location.endswith(url('reset_password')))
254 253
255 254 # GOOD KEY
256 255
257 256 key = User.get_by_username(username).api_key
258 257 response = self.app.get(url(controller='login',
259 258 action='password_reset_confirmation',
260 259 key=key))
261 260 self.assertEqual(response.status, '302 Found')
262 261 self.assertTrue(response.location.endswith(url('login_home')))
263 262
264 263 self.checkSessionFlash(response,
265 264 ('Your password reset was successful, '
266 265 'new password has been sent to your email'))
267 266
268 267 response = response.follow()
@@ -1,715 +1,715 b''
1 1 import os
2 2 import unittest
3 3 from rhodecode.tests import *
4 4
5 5 from rhodecode.model.repos_group import ReposGroupModel
6 6 from rhodecode.model.repo import RepoModel
7 7 from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \
8 UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm
8 UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm,\
9 Repository
9 10 from sqlalchemy.exc import IntegrityError
10 11 from rhodecode.model.user import UserModel
11 12
12 13 from rhodecode.model.meta import Session
13 14 from rhodecode.model.notification import NotificationModel
14 15 from rhodecode.model.users_group import UsersGroupModel
15 16 from rhodecode.lib.auth import AuthUser
16 17
17 18
18 19 def _make_group(path, desc='desc', parent_id=None,
19 20 skip_if_exists=False):
20 21
21 22 gr = RepoGroup.get_by_group_name(path)
22 23 if gr and skip_if_exists:
23 24 return gr
24 25
25 26 gr = ReposGroupModel().create(path, desc, parent_id)
26 27 return gr
27 28
28 29
29 30 class TestReposGroups(unittest.TestCase):
30 31
31 32 def setUp(self):
32 33 self.g1 = _make_group('test1', skip_if_exists=True)
33 34 Session.commit()
34 35 self.g2 = _make_group('test2', skip_if_exists=True)
35 36 Session.commit()
36 37 self.g3 = _make_group('test3', skip_if_exists=True)
37 38 Session.commit()
38 39
39 40 def tearDown(self):
40 41 print 'out'
41 42
42 43 def __check_path(self, *path):
43 44 """
44 45 Checks the path for existance !
45 46 """
46 47 path = [TESTS_TMP_PATH] + list(path)
47 48 path = os.path.join(*path)
48 49 return os.path.isdir(path)
49 50
50 51 def _check_folders(self):
51 52 print os.listdir(TESTS_TMP_PATH)
52 53
53 54 def __delete_group(self, id_):
54 55 ReposGroupModel().delete(id_)
55 56
56 57 def __update_group(self, id_, path, desc='desc', parent_id=None):
57 58 form_data = dict(
58 59 group_name=path,
59 60 group_description=desc,
60 61 group_parent_id=parent_id,
61 62 perms_updates=[],
62 63 perms_new=[]
63 64 )
64 65 gr = ReposGroupModel().update(id_, form_data)
65 66 return gr
66 67
67 68 def test_create_group(self):
68 69 g = _make_group('newGroup')
69 70 self.assertEqual(g.full_path, 'newGroup')
70 71
71 72 self.assertTrue(self.__check_path('newGroup'))
72 73
73 74 def test_create_same_name_group(self):
74 75 self.assertRaises(IntegrityError, lambda:_make_group('newGroup'))
75 76 Session.rollback()
76 77
77 78 def test_same_subgroup(self):
78 79 sg1 = _make_group('sub1', parent_id=self.g1.group_id)
79 80 self.assertEqual(sg1.parent_group, self.g1)
80 81 self.assertEqual(sg1.full_path, 'test1/sub1')
81 82 self.assertTrue(self.__check_path('test1', 'sub1'))
82 83
83 84 ssg1 = _make_group('subsub1', parent_id=sg1.group_id)
84 85 self.assertEqual(ssg1.parent_group, sg1)
85 86 self.assertEqual(ssg1.full_path, 'test1/sub1/subsub1')
86 87 self.assertTrue(self.__check_path('test1', 'sub1', 'subsub1'))
87 88
88 89 def test_remove_group(self):
89 90 sg1 = _make_group('deleteme')
90 91 self.__delete_group(sg1.group_id)
91 92
92 93 self.assertEqual(RepoGroup.get(sg1.group_id), None)
93 94 self.assertFalse(self.__check_path('deteteme'))
94 95
95 96 sg1 = _make_group('deleteme', parent_id=self.g1.group_id)
96 97 self.__delete_group(sg1.group_id)
97 98
98 99 self.assertEqual(RepoGroup.get(sg1.group_id), None)
99 100 self.assertFalse(self.__check_path('test1', 'deteteme'))
100 101
101 102 def test_rename_single_group(self):
102 103 sg1 = _make_group('initial')
103 104
104 105 new_sg1 = self.__update_group(sg1.group_id, 'after')
105 106 self.assertTrue(self.__check_path('after'))
106 107 self.assertEqual(RepoGroup.get_by_group_name('initial'), None)
107 108
108 109 def test_update_group_parent(self):
109 110
110 111 sg1 = _make_group('initial', parent_id=self.g1.group_id)
111 112
112 113 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g1.group_id)
113 114 self.assertTrue(self.__check_path('test1', 'after'))
114 115 self.assertEqual(RepoGroup.get_by_group_name('test1/initial'), None)
115 116
116 117 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g3.group_id)
117 118 self.assertTrue(self.__check_path('test3', 'after'))
118 119 self.assertEqual(RepoGroup.get_by_group_name('test3/initial'), None)
119 120
120 121 new_sg1 = self.__update_group(sg1.group_id, 'hello')
121 122 self.assertTrue(self.__check_path('hello'))
122 123
123 124 self.assertEqual(RepoGroup.get_by_group_name('hello'), new_sg1)
124 125
125 126 def test_subgrouping_with_repo(self):
126 127
127 128 g1 = _make_group('g1')
128 129 g2 = _make_group('g2')
129 130
130 131 # create new repo
131 132 form_data = dict(repo_name='john',
132 133 repo_name_full='john',
133 134 fork_name=None,
134 135 description=None,
135 136 repo_group=None,
136 137 private=False,
137 138 repo_type='hg',
138 139 clone_uri=None)
139 140 cur_user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
140 141 r = RepoModel().create(form_data, cur_user)
141 142
142 143 self.assertEqual(r.repo_name, 'john')
143 144
144 145 # put repo into group
145 146 form_data = form_data
146 147 form_data['repo_group'] = g1.group_id
147 148 form_data['perms_new'] = []
148 149 form_data['perms_updates'] = []
149 150 RepoModel().update(r.repo_name, form_data)
150 151 self.assertEqual(r.repo_name, 'g1/john')
151 152
152 153 self.__update_group(g1.group_id, 'g1', parent_id=g2.group_id)
153 154 self.assertTrue(self.__check_path('g2', 'g1'))
154 155
155 156 # test repo
156 self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name))
157
157 self.assertEqual(r.repo_name, RepoGroup.url_sep().join(['g2', 'g1', r.just_name]))
158 158
159 159 def test_move_to_root(self):
160 160 g1 = _make_group('t11')
161 161 Session.commit()
162 g2 = _make_group('t22',parent_id=g1.group_id)
162 g2 = _make_group('t22', parent_id=g1.group_id)
163 163 Session.commit()
164 164
165 self.assertEqual(g2.full_path,'t11/t22')
165 self.assertEqual(g2.full_path, 't11/t22')
166 166 self.assertTrue(self.__check_path('t11', 't22'))
167 167
168 168 g2 = self.__update_group(g2.group_id, 'g22', parent_id=None)
169 169 Session.commit()
170 170
171 self.assertEqual(g2.group_name,'g22')
171 self.assertEqual(g2.group_name, 'g22')
172 172 # we moved out group from t1 to '' so it's full path should be 'g2'
173 self.assertEqual(g2.full_path,'g22')
173 self.assertEqual(g2.full_path, 'g22')
174 174 self.assertFalse(self.__check_path('t11', 't22'))
175 175 self.assertTrue(self.__check_path('g22'))
176 176
177 177
178 178 class TestUser(unittest.TestCase):
179 179 def __init__(self, methodName='runTest'):
180 180 Session.remove()
181 181 super(TestUser, self).__init__(methodName=methodName)
182 182
183 183 def test_create_and_remove(self):
184 184 usr = UserModel().create_or_update(username=u'test_user', password=u'qweqwe',
185 185 email=u'u232@rhodecode.org',
186 186 name=u'u1', lastname=u'u1')
187 187 Session.commit()
188 188 self.assertEqual(User.get_by_username(u'test_user'), usr)
189 189
190 190 # make users group
191 191 users_group = UsersGroupModel().create('some_example_group')
192 192 Session.commit()
193 193
194 194 UsersGroupModel().add_user_to_group(users_group, usr)
195 195 Session.commit()
196 196
197 197 self.assertEqual(UsersGroup.get(users_group.users_group_id), users_group)
198 198 self.assertEqual(UsersGroupMember.query().count(), 1)
199 199 UserModel().delete(usr.user_id)
200 200 Session.commit()
201 201
202 202 self.assertEqual(UsersGroupMember.query().all(), [])
203 203
204 204
205 205 class TestNotifications(unittest.TestCase):
206 206
207 207 def __init__(self, methodName='runTest'):
208 208 Session.remove()
209 209 self.u1 = UserModel().create_or_update(username=u'u1',
210 210 password=u'qweqwe',
211 211 email=u'u1@rhodecode.org',
212 212 name=u'u1', lastname=u'u1')
213 213 Session.commit()
214 214 self.u1 = self.u1.user_id
215 215
216 216 self.u2 = UserModel().create_or_update(username=u'u2',
217 217 password=u'qweqwe',
218 218 email=u'u2@rhodecode.org',
219 219 name=u'u2', lastname=u'u3')
220 220 Session.commit()
221 221 self.u2 = self.u2.user_id
222 222
223 223 self.u3 = UserModel().create_or_update(username=u'u3',
224 224 password=u'qweqwe',
225 225 email=u'u3@rhodecode.org',
226 226 name=u'u3', lastname=u'u3')
227 227 Session.commit()
228 228 self.u3 = self.u3.user_id
229 229
230 230 super(TestNotifications, self).__init__(methodName=methodName)
231 231
232 232 def _clean_notifications(self):
233 233 for n in Notification.query().all():
234 234 Session.delete(n)
235 235
236 236 Session.commit()
237 237 self.assertEqual(Notification.query().all(), [])
238 238
239 239 def tearDown(self):
240 240 self._clean_notifications()
241 241
242 242 def test_create_notification(self):
243 243 self.assertEqual([], Notification.query().all())
244 244 self.assertEqual([], UserNotification.query().all())
245 245
246 246 usrs = [self.u1, self.u2]
247 247 notification = NotificationModel().create(created_by=self.u1,
248 248 subject=u'subj', body=u'hi there',
249 249 recipients=usrs)
250 250 Session.commit()
251 251 u1 = User.get(self.u1)
252 252 u2 = User.get(self.u2)
253 253 u3 = User.get(self.u3)
254 254 notifications = Notification.query().all()
255 255 self.assertEqual(len(notifications), 1)
256 256
257 257 unotification = UserNotification.query()\
258 258 .filter(UserNotification.notification == notification).all()
259 259
260 260 self.assertEqual(notifications[0].recipients, [u1, u2])
261 261 self.assertEqual(notification.notification_id,
262 262 notifications[0].notification_id)
263 263 self.assertEqual(len(unotification), len(usrs))
264 264 self.assertEqual([x.user.user_id for x in unotification], usrs)
265 265
266 266 def test_user_notifications(self):
267 267 self.assertEqual([], Notification.query().all())
268 268 self.assertEqual([], UserNotification.query().all())
269 269
270 270 notification1 = NotificationModel().create(created_by=self.u1,
271 271 subject=u'subj', body=u'hi there1',
272 272 recipients=[self.u3])
273 273 Session.commit()
274 274 notification2 = NotificationModel().create(created_by=self.u1,
275 275 subject=u'subj', body=u'hi there2',
276 276 recipients=[self.u3])
277 277 Session.commit()
278 278 u3 = Session.query(User).get(self.u3)
279 279
280 280 self.assertEqual(sorted([x.notification for x in u3.notifications]),
281 281 sorted([notification2, notification1]))
282 282
283 283 def test_delete_notifications(self):
284 284 self.assertEqual([], Notification.query().all())
285 285 self.assertEqual([], UserNotification.query().all())
286 286
287 287 notification = NotificationModel().create(created_by=self.u1,
288 288 subject=u'title', body=u'hi there3',
289 289 recipients=[self.u3, self.u1, self.u2])
290 290 Session.commit()
291 291 notifications = Notification.query().all()
292 292 self.assertTrue(notification in notifications)
293 293
294 294 Notification.delete(notification.notification_id)
295 295 Session.commit()
296 296
297 297 notifications = Notification.query().all()
298 298 self.assertFalse(notification in notifications)
299 299
300 300 un = UserNotification.query().filter(UserNotification.notification
301 301 == notification).all()
302 302 self.assertEqual(un, [])
303 303
304 304 def test_delete_association(self):
305 305
306 306 self.assertEqual([], Notification.query().all())
307 307 self.assertEqual([], UserNotification.query().all())
308 308
309 309 notification = NotificationModel().create(created_by=self.u1,
310 310 subject=u'title', body=u'hi there3',
311 311 recipients=[self.u3, self.u1, self.u2])
312 312 Session.commit()
313 313
314 314 unotification = UserNotification.query()\
315 315 .filter(UserNotification.notification ==
316 316 notification)\
317 317 .filter(UserNotification.user_id == self.u3)\
318 318 .scalar()
319 319
320 320 self.assertEqual(unotification.user_id, self.u3)
321 321
322 322 NotificationModel().delete(self.u3,
323 323 notification.notification_id)
324 324 Session.commit()
325 325
326 326 u3notification = UserNotification.query()\
327 327 .filter(UserNotification.notification ==
328 328 notification)\
329 329 .filter(UserNotification.user_id == self.u3)\
330 330 .scalar()
331 331
332 332 self.assertEqual(u3notification, None)
333 333
334 334 # notification object is still there
335 335 self.assertEqual(Notification.query().all(), [notification])
336 336
337 337 #u1 and u2 still have assignments
338 338 u1notification = UserNotification.query()\
339 339 .filter(UserNotification.notification ==
340 340 notification)\
341 341 .filter(UserNotification.user_id == self.u1)\
342 342 .scalar()
343 343 self.assertNotEqual(u1notification, None)
344 344 u2notification = UserNotification.query()\
345 345 .filter(UserNotification.notification ==
346 346 notification)\
347 347 .filter(UserNotification.user_id == self.u2)\
348 348 .scalar()
349 349 self.assertNotEqual(u2notification, None)
350 350
351 351 def test_notification_counter(self):
352 352 self._clean_notifications()
353 353 self.assertEqual([], Notification.query().all())
354 354 self.assertEqual([], UserNotification.query().all())
355 355
356 356 NotificationModel().create(created_by=self.u1,
357 357 subject=u'title', body=u'hi there_delete',
358 358 recipients=[self.u3, self.u1])
359 359 Session.commit()
360 360
361 361 self.assertEqual(NotificationModel()
362 362 .get_unread_cnt_for_user(self.u1), 1)
363 363 self.assertEqual(NotificationModel()
364 364 .get_unread_cnt_for_user(self.u2), 0)
365 365 self.assertEqual(NotificationModel()
366 366 .get_unread_cnt_for_user(self.u3), 1)
367 367
368 368 notification = NotificationModel().create(created_by=self.u1,
369 369 subject=u'title', body=u'hi there3',
370 370 recipients=[self.u3, self.u1, self.u2])
371 371 Session.commit()
372 372
373 373 self.assertEqual(NotificationModel()
374 374 .get_unread_cnt_for_user(self.u1), 2)
375 375 self.assertEqual(NotificationModel()
376 376 .get_unread_cnt_for_user(self.u2), 1)
377 377 self.assertEqual(NotificationModel()
378 378 .get_unread_cnt_for_user(self.u3), 2)
379 379
380 380
381 381 class TestUsers(unittest.TestCase):
382 382
383 383 def __init__(self, methodName='runTest'):
384 384 super(TestUsers, self).__init__(methodName=methodName)
385 385
386 386 def setUp(self):
387 387 self.u1 = UserModel().create_or_update(username=u'u1',
388 388 password=u'qweqwe',
389 389 email=u'u1@rhodecode.org',
390 390 name=u'u1', lastname=u'u1')
391 391
392 392 def tearDown(self):
393 393 perm = Permission.query().all()
394 394 for p in perm:
395 395 UserModel().revoke_perm(self.u1, p)
396 396
397 397 UserModel().delete(self.u1)
398 398 Session.commit()
399 399
400 400 def test_add_perm(self):
401 401 perm = Permission.query().all()[0]
402 402 UserModel().grant_perm(self.u1, perm)
403 403 Session.commit()
404 404 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
405 405
406 406 def test_has_perm(self):
407 407 perm = Permission.query().all()
408 408 for p in perm:
409 409 has_p = UserModel().has_perm(self.u1, p)
410 410 self.assertEqual(False, has_p)
411 411
412 412 def test_revoke_perm(self):
413 413 perm = Permission.query().all()[0]
414 414 UserModel().grant_perm(self.u1, perm)
415 415 Session.commit()
416 416 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
417 417
418 418 #revoke
419 419 UserModel().revoke_perm(self.u1, perm)
420 420 Session.commit()
421 421 self.assertEqual(UserModel().has_perm(self.u1, perm), False)
422 422
423 423
424 424 class TestPermissions(unittest.TestCase):
425 425 def __init__(self, methodName='runTest'):
426 426 super(TestPermissions, self).__init__(methodName=methodName)
427 427
428 428 def setUp(self):
429 429 self.u1 = UserModel().create_or_update(
430 430 username=u'u1', password=u'qweqwe',
431 431 email=u'u1@rhodecode.org', name=u'u1', lastname=u'u1'
432 432 )
433 433 self.u2 = UserModel().create_or_update(
434 434 username=u'u2', password=u'qweqwe',
435 435 email=u'u2@rhodecode.org', name=u'u2', lastname=u'u2'
436 436 )
437 437 self.anon = User.get_by_username('default')
438 438 self.a1 = UserModel().create_or_update(
439 439 username=u'a1', password=u'qweqwe',
440 440 email=u'a1@rhodecode.org', name=u'a1', lastname=u'a1', admin=True
441 441 )
442 442 Session.commit()
443 443
444 444 def tearDown(self):
445 445 if hasattr(self, 'test_repo'):
446 446 RepoModel().delete(repo=self.test_repo)
447 447 UserModel().delete(self.u1)
448 448 UserModel().delete(self.u2)
449 449 UserModel().delete(self.a1)
450 450 if hasattr(self, 'g1'):
451 451 ReposGroupModel().delete(self.g1.group_id)
452 452 if hasattr(self, 'g2'):
453 453 ReposGroupModel().delete(self.g2.group_id)
454 454
455 455 if hasattr(self, 'ug1'):
456 456 UsersGroupModel().delete(self.ug1, force=True)
457 457
458 458 Session.commit()
459 459
460 460 def test_default_perms_set(self):
461 461 u1_auth = AuthUser(user_id=self.u1.user_id)
462 462 perms = {
463 463 'repositories_groups': {},
464 464 'global': set([u'hg.create.repository', u'repository.read',
465 465 u'hg.register.manual_activate']),
466 466 'repositories': {u'vcs_test_hg': u'repository.read'}
467 467 }
468 468 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
469 469 perms['repositories'][HG_REPO])
470 470 new_perm = 'repository.write'
471 471 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
472 472 Session.commit()
473 473
474 474 u1_auth = AuthUser(user_id=self.u1.user_id)
475 475 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], new_perm)
476 476
477 477 def test_default_admin_perms_set(self):
478 478 a1_auth = AuthUser(user_id=self.a1.user_id)
479 479 perms = {
480 480 'repositories_groups': {},
481 481 'global': set([u'hg.admin']),
482 482 'repositories': {u'vcs_test_hg': u'repository.admin'}
483 483 }
484 484 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
485 485 perms['repositories'][HG_REPO])
486 486 new_perm = 'repository.write'
487 487 RepoModel().grant_user_permission(repo=HG_REPO, user=self.a1, perm=new_perm)
488 488 Session.commit()
489 489 # cannot really downgrade admins permissions !? they still get's set as
490 490 # admin !
491 491 u1_auth = AuthUser(user_id=self.a1.user_id)
492 492 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
493 493 perms['repositories'][HG_REPO])
494 494
495 495 def test_default_group_perms(self):
496 496 self.g1 = _make_group('test1', skip_if_exists=True)
497 497 self.g2 = _make_group('test2', skip_if_exists=True)
498 498 u1_auth = AuthUser(user_id=self.u1.user_id)
499 499 perms = {
500 500 'repositories_groups': {u'test1': 'group.read', u'test2': 'group.read'},
501 501 'global': set([u'hg.create.repository', u'repository.read', u'hg.register.manual_activate']),
502 502 'repositories': {u'vcs_test_hg': u'repository.read'}
503 503 }
504 504 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
505 505 perms['repositories'][HG_REPO])
506 506 self.assertEqual(u1_auth.permissions['repositories_groups'],
507 507 perms['repositories_groups'])
508 508
509 509 def test_default_admin_group_perms(self):
510 510 self.g1 = _make_group('test1', skip_if_exists=True)
511 511 self.g2 = _make_group('test2', skip_if_exists=True)
512 512 a1_auth = AuthUser(user_id=self.a1.user_id)
513 513 perms = {
514 514 'repositories_groups': {u'test1': 'group.admin', u'test2': 'group.admin'},
515 515 'global': set(['hg.admin']),
516 516 'repositories': {u'vcs_test_hg': 'repository.admin'}
517 517 }
518 518
519 519 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
520 520 perms['repositories'][HG_REPO])
521 521 self.assertEqual(a1_auth.permissions['repositories_groups'],
522 522 perms['repositories_groups'])
523 523
524 524 def test_propagated_permission_from_users_group(self):
525 525 # make group
526 526 self.ug1 = UsersGroupModel().create('G1')
527 527 # add user to group
528 528 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
529 529
530 530 # set permission to lower
531 531 new_perm = 'repository.none'
532 532 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
533 533 Session.commit()
534 534 u1_auth = AuthUser(user_id=self.u1.user_id)
535 535 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
536 536 new_perm)
537 537
538 538 # grant perm for group this should override permission from user
539 539 new_perm = 'repository.write'
540 540 RepoModel().grant_users_group_permission(repo=HG_REPO,
541 541 group_name=self.ug1,
542 542 perm=new_perm)
543 543 # check perms
544 544 u1_auth = AuthUser(user_id=self.u1.user_id)
545 545 perms = {
546 546 'repositories_groups': {},
547 547 'global': set([u'hg.create.repository', u'repository.read',
548 548 u'hg.register.manual_activate']),
549 549 'repositories': {u'vcs_test_hg': u'repository.read'}
550 550 }
551 551 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
552 552 new_perm)
553 553 self.assertEqual(u1_auth.permissions['repositories_groups'],
554 554 perms['repositories_groups'])
555 555
556 556 def test_propagated_permission_from_users_group_lower_weight(self):
557 557 # make group
558 558 self.ug1 = UsersGroupModel().create('G1')
559 559 # add user to group
560 560 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
561 561
562 562 # set permission to lower
563 563 new_perm_h = 'repository.write'
564 564 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
565 565 perm=new_perm_h)
566 566 Session.commit()
567 567 u1_auth = AuthUser(user_id=self.u1.user_id)
568 568 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
569 569 new_perm_h)
570 570
571 571 # grant perm for group this should NOT override permission from user
572 572 # since it's lower than granted
573 573 new_perm_l = 'repository.read'
574 574 RepoModel().grant_users_group_permission(repo=HG_REPO,
575 575 group_name=self.ug1,
576 576 perm=new_perm_l)
577 577 # check perms
578 578 u1_auth = AuthUser(user_id=self.u1.user_id)
579 579 perms = {
580 580 'repositories_groups': {},
581 581 'global': set([u'hg.create.repository', u'repository.read',
582 582 u'hg.register.manual_activate']),
583 583 'repositories': {u'vcs_test_hg': u'repository.write'}
584 584 }
585 585 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
586 586 new_perm_h)
587 587 self.assertEqual(u1_auth.permissions['repositories_groups'],
588 588 perms['repositories_groups'])
589 589
590 590 def test_repo_in_group_permissions(self):
591 591 self.g1 = _make_group('group1', skip_if_exists=True)
592 592 self.g2 = _make_group('group2', skip_if_exists=True)
593 593 Session.commit()
594 594 # both perms should be read !
595 595 u1_auth = AuthUser(user_id=self.u1.user_id)
596 596 self.assertEqual(u1_auth.permissions['repositories_groups'],
597 597 {u'group1': u'group.read', u'group2': u'group.read'})
598 598
599 599 a1_auth = AuthUser(user_id=self.anon.user_id)
600 600 self.assertEqual(a1_auth.permissions['repositories_groups'],
601 601 {u'group1': u'group.read', u'group2': u'group.read'})
602 602
603 603 #Change perms to none for both groups
604 604 ReposGroupModel().grant_user_permission(repos_group=self.g1,
605 605 user=self.anon,
606 606 perm='group.none')
607 607 ReposGroupModel().grant_user_permission(repos_group=self.g2,
608 608 user=self.anon,
609 609 perm='group.none')
610 610
611 611
612 612 u1_auth = AuthUser(user_id=self.u1.user_id)
613 613 self.assertEqual(u1_auth.permissions['repositories_groups'],
614 614 {u'group1': u'group.none', u'group2': u'group.none'})
615 615
616 616 a1_auth = AuthUser(user_id=self.anon.user_id)
617 617 self.assertEqual(a1_auth.permissions['repositories_groups'],
618 618 {u'group1': u'group.none', u'group2': u'group.none'})
619 619
620 620 # add repo to group
621 621 form_data = {
622 622 'repo_name':HG_REPO,
623 'repo_name_full':os.path.join(self.g1.group_name,HG_REPO),
623 'repo_name_full':RepoGroup.url_sep().join([self.g1.group_name,HG_REPO]),
624 624 'repo_type':'hg',
625 625 'clone_uri':'',
626 626 'repo_group':self.g1.group_id,
627 627 'description':'desc',
628 628 'private':False
629 629 }
630 630 self.test_repo = RepoModel().create(form_data, cur_user=self.u1)
631 631 Session.commit()
632 632
633 633 u1_auth = AuthUser(user_id=self.u1.user_id)
634 634 self.assertEqual(u1_auth.permissions['repositories_groups'],
635 635 {u'group1': u'group.none', u'group2': u'group.none'})
636 636
637 637 a1_auth = AuthUser(user_id=self.anon.user_id)
638 638 self.assertEqual(a1_auth.permissions['repositories_groups'],
639 639 {u'group1': u'group.none', u'group2': u'group.none'})
640 640
641 641 #grant permission for u2 !
642 642 ReposGroupModel().grant_user_permission(repos_group=self.g1,
643 643 user=self.u2,
644 644 perm='group.read')
645 645 ReposGroupModel().grant_user_permission(repos_group=self.g2,
646 646 user=self.u2,
647 647 perm='group.read')
648 648 Session.commit()
649 649 self.assertNotEqual(self.u1, self.u2)
650 650 #u1 and anon should have not change perms while u2 should !
651 651 u1_auth = AuthUser(user_id=self.u1.user_id)
652 652 self.assertEqual(u1_auth.permissions['repositories_groups'],
653 653 {u'group1': u'group.none', u'group2': u'group.none'})
654 654
655 655 u2_auth = AuthUser(user_id=self.u2.user_id)
656 656 self.assertEqual(u2_auth.permissions['repositories_groups'],
657 657 {u'group1': u'group.read', u'group2': u'group.read'})
658 658
659 659 a1_auth = AuthUser(user_id=self.anon.user_id)
660 660 self.assertEqual(a1_auth.permissions['repositories_groups'],
661 661 {u'group1': u'group.none', u'group2': u'group.none'})
662 662
663 663 def test_repo_group_user_as_user_group_member(self):
664 664 # create Group1
665 665 self.g1 = _make_group('group1', skip_if_exists=True)
666 666 Session.commit()
667 667 a1_auth = AuthUser(user_id=self.anon.user_id)
668 668
669 669 self.assertEqual(a1_auth.permissions['repositories_groups'],
670 670 {u'group1': u'group.read'})
671 671
672 672 # set default permission to none
673 673 ReposGroupModel().grant_user_permission(repos_group=self.g1,
674 674 user=self.anon,
675 675 perm='group.none')
676 676 # make group
677 677 self.ug1 = UsersGroupModel().create('G1')
678 678 # add user to group
679 679 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
680 680 Session.commit()
681 681
682 682 # check if user is in the group
683 683 membrs = [x.user_id for x in UsersGroupModel().get(self.ug1.users_group_id).members]
684 684 self.assertEqual(membrs, [self.u1.user_id])
685 685 # add some user to that group
686 686
687 687 # check his permissions
688 688 a1_auth = AuthUser(user_id=self.anon.user_id)
689 689 self.assertEqual(a1_auth.permissions['repositories_groups'],
690 690 {u'group1': u'group.none'})
691 691
692 692 u1_auth = AuthUser(user_id=self.u1.user_id)
693 693 self.assertEqual(u1_auth.permissions['repositories_groups'],
694 694 {u'group1': u'group.none'})
695 695
696 696 # grant ug1 read permissions for
697 697 ReposGroupModel().grant_users_group_permission(repos_group=self.g1,
698 698 group_name=self.ug1,
699 699 perm='group.read')
700 700 Session.commit()
701 701 # check if the
702 702 obj = Session.query(UsersGroupRepoGroupToPerm)\
703 703 .filter(UsersGroupRepoGroupToPerm.group == self.g1)\
704 704 .filter(UsersGroupRepoGroupToPerm.users_group == self.ug1)\
705 705 .scalar()
706 706 self.assertEqual(obj.permission.permission_name, 'group.read')
707 707
708 708 a1_auth = AuthUser(user_id=self.anon.user_id)
709 709
710 710 self.assertEqual(a1_auth.permissions['repositories_groups'],
711 711 {u'group1': u'group.none'})
712 712
713 713 u1_auth = AuthUser(user_id=self.u1.user_id)
714 714 self.assertEqual(u1_auth.permissions['repositories_groups'],
715 715 {u'group1': u'group.read'})
General Comments 0
You need to be logged in to leave comments. Login now