Show More
The requested changes are too big and content was truncated. Show full diff
@@ -0,0 +1,57 b'' | |||||
|
1 | # This program is free software: you can redistribute it and/or modify | |||
|
2 | # it under the terms of the GNU General Public License as published by | |||
|
3 | # the Free Software Foundation, either version 3 of the License, or | |||
|
4 | # (at your option) any later version. | |||
|
5 | # | |||
|
6 | # This program is distributed in the hope that it will be useful, | |||
|
7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
9 | # GNU General Public License for more details. | |||
|
10 | # | |||
|
11 | # You should have received a copy of the GNU General Public License | |||
|
12 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
13 | ||||
|
14 | """hooks: migrate internal hooks to kallithea namespace | |||
|
15 | ||||
|
16 | Revision ID: 7ba0d2cad930 | |||
|
17 | Revises: f62826179f39 | |||
|
18 | Create Date: 2021-01-11 00:10:13.576586 | |||
|
19 | ||||
|
20 | """ | |||
|
21 | ||||
|
22 | # The following opaque hexadecimal identifiers ("revisions") are used | |||
|
23 | # by Alembic to track this migration script and its relations to others. | |||
|
24 | revision = '7ba0d2cad930' | |||
|
25 | down_revision = 'f62826179f39' | |||
|
26 | branch_labels = None | |||
|
27 | depends_on = None | |||
|
28 | ||||
|
29 | from alembic import op | |||
|
30 | from sqlalchemy import MetaData, Table | |||
|
31 | ||||
|
32 | from kallithea.model import db | |||
|
33 | ||||
|
34 | ||||
|
35 | meta = MetaData() | |||
|
36 | ||||
|
37 | ||||
|
38 | def upgrade(): | |||
|
39 | meta.bind = op.get_bind() | |||
|
40 | ui = Table(db.Ui.__tablename__, meta, autoload=True) | |||
|
41 | ||||
|
42 | ui.update(values={ | |||
|
43 | 'ui_key': 'changegroup.kallithea_update', | |||
|
44 | 'ui_value': 'python:', # value in db isn't used | |||
|
45 | }).where(ui.c.ui_key == 'changegroup.update').execute() | |||
|
46 | ui.update(values={ | |||
|
47 | 'ui_key': 'changegroup.kallithea_repo_size', | |||
|
48 | 'ui_value': 'python:', # value in db isn't used | |||
|
49 | }).where(ui.c.ui_key == 'changegroup.repo_size').execute() | |||
|
50 | ||||
|
51 | # 642847355a10 moved these hooks out of db - remove old entries | |||
|
52 | ui.delete().where(ui.c.ui_key == 'changegroup.push_logger').execute() | |||
|
53 | ui.delete().where(ui.c.ui_key == 'outgoing.pull_logger').execute() | |||
|
54 | ||||
|
55 | ||||
|
56 | def downgrade(): | |||
|
57 | pass |
@@ -0,0 +1,73 b'' | |||||
|
1 | # This program is free software: you can redistribute it and/or modify | |||
|
2 | # it under the terms of the GNU General Public License as published by | |||
|
3 | # the Free Software Foundation, either version 3 of the License, or | |||
|
4 | # (at your option) any later version. | |||
|
5 | # | |||
|
6 | # This program is distributed in the hope that it will be useful, | |||
|
7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
9 | # GNU General Public License for more details. | |||
|
10 | # | |||
|
11 | # You should have received a copy of the GNU General Public License | |||
|
12 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
13 | ||||
|
14 | """add unique constraint on PullRequestReviewer | |||
|
15 | ||||
|
16 | Revision ID: f62826179f39 | |||
|
17 | Revises: a0a1bf09c143 | |||
|
18 | Create Date: 2020-06-15 12:30:37.420321 | |||
|
19 | ||||
|
20 | """ | |||
|
21 | ||||
|
22 | # The following opaque hexadecimal identifiers ("revisions") are used | |||
|
23 | # by Alembic to track this migration script and its relations to others. | |||
|
24 | revision = 'f62826179f39' | |||
|
25 | down_revision = 'a0a1bf09c143' | |||
|
26 | branch_labels = None | |||
|
27 | depends_on = None | |||
|
28 | ||||
|
29 | import sqlalchemy as sa | |||
|
30 | from alembic import op | |||
|
31 | ||||
|
32 | from kallithea.model import db | |||
|
33 | ||||
|
34 | ||||
|
35 | def upgrade(): | |||
|
36 | session = sa.orm.session.Session(bind=op.get_bind()) | |||
|
37 | ||||
|
38 | # there may be existing duplicates in the database, remove them first | |||
|
39 | ||||
|
40 | seen = set() | |||
|
41 | # duplicate_values contains one copy of each duplicated pair | |||
|
42 | duplicate_values = ( | |||
|
43 | session | |||
|
44 | .query(db.PullRequestReviewer.pull_request_id, db.PullRequestReviewer.user_id) | |||
|
45 | .group_by(db.PullRequestReviewer.pull_request_id, db.PullRequestReviewer.user_id) | |||
|
46 | .having(sa.func.count(db.PullRequestReviewer.pull_request_reviewers_id) > 1) | |||
|
47 | ) | |||
|
48 | ||||
|
49 | for pull_request_id, user_id in duplicate_values: | |||
|
50 | # duplicate_occurrences contains all db records of the duplicate_value | |||
|
51 | # currently being processed | |||
|
52 | duplicate_occurrences = ( | |||
|
53 | session | |||
|
54 | .query(db.PullRequestReviewer) | |||
|
55 | .filter(db.PullRequestReviewer.pull_request_id == pull_request_id) | |||
|
56 | .filter(db.PullRequestReviewer.user_id == user_id) | |||
|
57 | ) | |||
|
58 | for prr in duplicate_occurrences: | |||
|
59 | if (pull_request_id, user_id) in seen: | |||
|
60 | session.delete(prr) | |||
|
61 | else: | |||
|
62 | seen.add((pull_request_id, user_id)) | |||
|
63 | ||||
|
64 | session.commit() | |||
|
65 | ||||
|
66 | # after deleting all duplicates, add the unique constraint | |||
|
67 | with op.batch_alter_table('pull_request_reviewers', schema=None) as batch_op: | |||
|
68 | batch_op.create_unique_constraint(batch_op.f('uq_pull_request_reviewers_pull_request_id'), ['pull_request_id', 'user_id']) | |||
|
69 | ||||
|
70 | ||||
|
71 | def downgrade(): | |||
|
72 | with op.batch_alter_table('pull_request_reviewers', schema=None) as batch_op: | |||
|
73 | batch_op.drop_constraint(batch_op.f('uq_pull_request_reviewers_pull_request_id'), type_='unique') |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100644 |
|
NO CONTENT: new file 100644 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100755 |
|
NO CONTENT: new file 100755 | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: new file 100755 |
|
NO CONTENT: new file 100755 | ||
The requested commit or file is too big and content was truncated. Show full diff |
@@ -8,9 +8,6 b' omit =' | |||||
8 | kallithea/lib/dbmigrate/* |
|
8 | kallithea/lib/dbmigrate/* | |
9 | # the tests themselves should not be part of the coverage report |
|
9 | # the tests themselves should not be part of the coverage report | |
10 | kallithea/tests/* |
|
10 | kallithea/tests/* | |
11 | # the scm hooks are not run in the kallithea process |
|
|||
12 | kallithea/config/post_receive_tmpl.py |
|
|||
13 | kallithea/config/pre_receive_tmpl.py |
|
|||
14 |
|
11 | |||
15 | # same omit lines should be present in sections 'run' and 'report' |
|
12 | # same omit lines should be present in sections 'run' and 'report' | |
16 | [report] |
|
13 | [report] | |
@@ -23,9 +20,6 b' omit =' | |||||
23 | kallithea/lib/dbmigrate/* |
|
20 | kallithea/lib/dbmigrate/* | |
24 | # the tests themselves should not be part of the coverage report |
|
21 | # the tests themselves should not be part of the coverage report | |
25 | kallithea/tests/* |
|
22 | kallithea/tests/* | |
26 | # the scm hooks are not run in the kallithea process |
|
|||
27 | kallithea/config/post_receive_tmpl.py |
|
|||
28 | kallithea/config/pre_receive_tmpl.py |
|
|||
29 |
|
23 | |||
30 | [paths] |
|
24 | [paths] | |
31 | source = |
|
25 | source = |
@@ -10,16 +10,15 b' syntax: glob' | |||||
10 | *.rej |
|
10 | *.rej | |
11 | *.bak |
|
11 | *.bak | |
12 | .eggs/ |
|
12 | .eggs/ | |
13 | tarballcache/ |
|
|||
14 |
|
13 | |||
15 | syntax: regexp |
|
14 | syntax: regexp | |
16 |
^ |
|
15 | ^extensions\.py$ | |
17 | ^build |
|
16 | ^build$ | |
18 |
^dist |
|
17 | ^dist$ | |
19 |
^docs/build |
|
18 | ^docs/build$ | |
20 |
^docs/_build |
|
19 | ^docs/_build$ | |
21 | ^data$ |
|
20 | ^data$ | |
22 |
^sql_dumps |
|
21 | ^sql_dumps$ | |
23 | ^\.settings$ |
|
22 | ^\.settings$ | |
24 | ^\.project$ |
|
23 | ^\.project$ | |
25 | ^\.pydevproject$ |
|
24 | ^\.pydevproject$ | |
@@ -48,8 +47,13 b' syntax: regexp' | |||||
48 | ^test\.db$ |
|
47 | ^test\.db$ | |
49 | ^Kallithea\.egg-info$ |
|
48 | ^Kallithea\.egg-info$ | |
50 | ^my\.ini$ |
|
49 | ^my\.ini$ | |
51 | ^fabfile.py |
|
50 | ^fabfile\.py$ | |
52 | ^\.idea$ |
|
51 | ^\.idea$ | |
53 | ^\.cache$ |
|
52 | ^\.cache$ | |
54 | ^\.pytest_cache$ |
|
53 | ^\.pytest_cache$ | |
|
54 | ^venv$ | |||
55 | /__pycache__$ |
|
55 | /__pycache__$ | |
|
56 | ^deps\.dot$ | |||
|
57 | ^deps\.svg$ | |||
|
58 | ^deps\.txt$ | |||
|
59 | ^\.pytype/ |
@@ -1,19 +1,26 b'' | |||||
1 | List of contributors to Kallithea project: |
|
1 | List of contributors to Kallithea project: | |
2 |
|
2 | |||
3 |
Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-202 |
|
3 | Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2021 | |
4 |
Mads Kiilerich <mads@kiilerich.com> 2016-202 |
|
4 | Mads Kiilerich <mads@kiilerich.com> 2016-2021 | |
|
5 | ssantos <ssantos@web.de> 2018-2021 | |||
|
6 | Private <adamantine.sword@gmail.com> 2019-2021 | |||
|
7 | Étienne Gilli <etienne@gilli.io> 2020-2021 | |||
|
8 | fresh <fresh190@protonmail.com> 2020-2021 | |||
|
9 | robertus <robertuss12@gmail.com> 2020-2021 | |||
|
10 | Eugenia Russell <eugenia.russell2019@gmail.com> 2021 | |||
|
11 | Michalis <michalisntovas@yahoo.gr> 2021 | |||
|
12 | vs <vsuhachev@yandex.ru> 2021 | |||
|
13 | Александр <akonn7@mail.ru> 2021 | |||
5 | Asterios Dimitriou <steve@pci.gr> 2016-2017 2020 |
|
14 | Asterios Dimitriou <steve@pci.gr> 2016-2017 2020 | |
6 | Allan Nordhøy <epost@anotheragency.no> 2017-2020 |
|
15 | Allan Nordhøy <epost@anotheragency.no> 2017-2020 | |
7 | Anton Schur <tonich.sh@gmail.com> 2017 2020 |
|
16 | Anton Schur <tonich.sh@gmail.com> 2017 2020 | |
8 | ssantos <ssantos@web.de> 2018-2020 |
|
|||
9 | Manuel Jacob <me@manueljacob.de> 2019-2020 |
|
17 | Manuel Jacob <me@manueljacob.de> 2019-2020 | |
10 |
|
|
18 | Artem <kovalevartem.ru@gmail.com> 2020 | |
11 | David Ignjić <ignjic@gmail.com> 2020 |
|
19 | David Ignjić <ignjic@gmail.com> 2020 | |
12 | Dennis Fink <dennis.fink@c3l.lu> 2020 |
|
20 | Dennis Fink <dennis.fink@c3l.lu> 2020 | |
13 | Étienne Gilli <etienne@gilli.io> 2020 |
|
|||
14 | J. Lavoie <j.lavoie@net-c.ca> 2020 |
|
21 | J. Lavoie <j.lavoie@net-c.ca> 2020 | |
15 | robertus <robertuss12@gmail.com> 2020 |
|
|||
16 | Ross Thomas <ross@lns-nevasoft.com> 2020 |
|
22 | Ross Thomas <ross@lns-nevasoft.com> 2020 | |
|
23 | Tim Ooms <tatankat@users.noreply.github.com> 2020 | |||
17 | Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019 |
|
24 | Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019 | |
18 | Étienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019 |
|
25 | Étienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019 | |
19 | Adi Kriegisch <adi@cg.tuwien.ac.at> 2019 |
|
26 | Adi Kriegisch <adi@cg.tuwien.ac.at> 2019 |
@@ -18,7 +18,6 b' recursive-include docs *' | |||||
18 | recursive-include init.d * |
|
18 | recursive-include init.d * | |
19 | recursive-include kallithea/alembic * |
|
19 | recursive-include kallithea/alembic * | |
20 | include kallithea/bin/ldap_sync.conf |
|
20 | include kallithea/bin/ldap_sync.conf | |
21 | include kallithea/lib/paster_commands/template.ini.mako |
|
|||
22 | recursive-include kallithea/front-end * |
|
21 | recursive-include kallithea/front-end * | |
23 | recursive-include kallithea/i18n * |
|
22 | recursive-include kallithea/i18n * | |
24 | recursive-include kallithea/public * |
|
23 | recursive-include kallithea/public * |
@@ -74,8 +74,8 b' Kallithea features' | |||||
74 | web interface using simple editor or upload binary files using simple form. |
|
74 | web interface using simple editor or upload binary files using simple form. | |
75 | - Powerful pull request driven review system with inline commenting, changeset |
|
75 | - Powerful pull request driven review system with inline commenting, changeset | |
76 | statuses, and notification system. |
|
76 | statuses, and notification system. | |
77 |
- Importing and syncing repositories from remote locations for Git_ |
|
77 | - Importing and syncing repositories from remote locations for Git_ and | |
78 | and Subversion. |
|
78 | Mercurial_. | |
79 | - Mako templates let you customize the look and feel of the application. |
|
79 | - Mako templates let you customize the look and feel of the application. | |
80 | - Beautiful diffs, annotations and source code browsing all colored by |
|
80 | - Beautiful diffs, annotations and source code browsing all colored by | |
81 | pygments. Raw diffs are made in Git-diff format for both VCS systems, |
|
81 | pygments. Raw diffs are made in Git-diff format for both VCS systems, | |
@@ -175,7 +175,6 b' of Kallithea.' | |||||
175 | .. _Mercurial: http://mercurial.selenic.com/ |
|
175 | .. _Mercurial: http://mercurial.selenic.com/ | |
176 | .. _Bitbucket: http://bitbucket.org/ |
|
176 | .. _Bitbucket: http://bitbucket.org/ | |
177 | .. _GitHub: http://github.com/ |
|
177 | .. _GitHub: http://github.com/ | |
178 | .. _Subversion: http://subversion.tigris.org/ |
|
|||
179 | .. _Git: http://git-scm.com/ |
|
178 | .. _Git: http://git-scm.com/ | |
180 | .. _Celery: http://celeryproject.org/ |
|
179 | .. _Celery: http://celeryproject.org/ | |
181 | .. _Software Freedom Conservancy: http://sfconservancy.org/ |
|
180 | .. _Software Freedom Conservancy: http://sfconservancy.org/ |
@@ -1,9 +1,9 b'' | |||||
1 |
pytest >= 4.6.6, < 5. |
|
1 | pytest >= 4.6.6, < 5.5 | |
2 | pytest-sugar >= 0.9.2, < 0.10 |
|
2 | pytest-sugar >= 0.9.2, < 0.10 | |
3 | pytest-benchmark >= 3.2.2, < 3.3 |
|
3 | pytest-benchmark >= 3.2.2, < 3.3 | |
4 | pytest-localserver >= 0.5.0, < 0.6 |
|
4 | pytest-localserver >= 0.5.0, < 0.6 | |
5 | mock >= 3.0.0, < 4.1 |
|
5 | mock >= 3.0.0, < 4.1 | |
6 |
Sphinx >= 1.8.0, < |
|
6 | Sphinx >= 1.8.0, < 3.1 | |
7 | WebTest >= 2.0.6, < 2.1 |
|
7 | WebTest >= 2.0.6, < 2.1 | |
8 |
isort == |
|
8 | isort == 5.1.2 | |
9 |
pyflakes == 2. |
|
9 | pyflakes == 2.2.0 |
@@ -67,11 +67,11 b' smtp_use_tls = false' | |||||
67 | host = 0.0.0.0 |
|
67 | host = 0.0.0.0 | |
68 | port = 5000 |
|
68 | port = 5000 | |
69 |
|
69 | |||
70 | ## WAITRESS ## |
|
70 | ## Gearbox serve uses the Waitress web server ## | |
71 | use = egg:waitress#main |
|
71 | use = egg:waitress#main | |
72 | ## number of worker threads |
|
72 | ## avoid multi threading | |
73 | threads = 1 |
|
73 | threads = 1 | |
74 | ## MAX BODY SIZE 100GB |
|
74 | ## allow push of repos bigger than the default of 1 GB | |
75 | max_request_body_size = 107374182400 |
|
75 | max_request_body_size = 107374182400 | |
76 | ## use poll instead of select, fixes fd limits, may not work on old |
|
76 | ## use poll instead of select, fixes fd limits, may not work on old | |
77 | ## windows systems. |
|
77 | ## windows systems. | |
@@ -81,6 +81,7 b' max_request_body_size = 107374182400' | |||||
81 | #[filter:proxy-prefix] |
|
81 | #[filter:proxy-prefix] | |
82 | #use = egg:PasteDeploy#prefix |
|
82 | #use = egg:PasteDeploy#prefix | |
83 | #prefix = /<your-prefix> |
|
83 | #prefix = /<your-prefix> | |
|
84 | #translate_forwarded_server = False | |||
84 |
|
85 | |||
85 | [app:main] |
|
86 | [app:main] | |
86 | use = egg:kallithea |
|
87 | use = egg:kallithea | |
@@ -102,7 +103,7 b' cache_dir = %(here)s/data' | |||||
102 | index_dir = %(here)s/data/index |
|
103 | index_dir = %(here)s/data/index | |
103 |
|
104 | |||
104 | ## uncomment and set this path to use archive download cache |
|
105 | ## uncomment and set this path to use archive download cache | |
105 | archive_cache_dir = %(here)s/tarballcache |
|
106 | archive_cache_dir = %(here)s/data/tarballcache | |
106 |
|
107 | |||
107 | ## change this to unique ID for security |
|
108 | ## change this to unique ID for security | |
108 | #app_instance_uuid = VERY-SECRET |
|
109 | #app_instance_uuid = VERY-SECRET | |
@@ -111,11 +112,17 b' app_instance_uuid = development-not-secr' | |||||
111 | ## cut off limit for large diffs (size in bytes) |
|
112 | ## cut off limit for large diffs (size in bytes) | |
112 | cut_off_limit = 256000 |
|
113 | cut_off_limit = 256000 | |
113 |
|
114 | |||
114 | ## force https in Kallithea, fixes https redirects, assumes it's always https |
|
115 | ## WSGI environment variable to get the IP address of the client (default REMOTE_ADDR) | |
115 | force_https = false |
|
116 | #remote_addr_variable = HTTP_X_FORWARDED_FOR | |
|
117 | ||||
|
118 | ## WSGI environment variable to get the protocol (http or https) of the client connection (default wsgi.url_scheme) | |||
|
119 | #url_scheme_variable = HTTP_X_FORWARDED_PROTO | |||
116 |
|
120 | |||
117 | ## use Strict-Transport-Security headers |
|
121 | ## always pretend the client connected using HTTPS (default false) | |
118 | use_htsts = false |
|
122 | #force_https = true | |
|
123 | ||||
|
124 | ## use Strict-Transport-Security headers (default false) | |||
|
125 | #use_htsts = true | |||
119 |
|
126 | |||
120 | ## number of commits stats will parse on each iteration |
|
127 | ## number of commits stats will parse on each iteration | |
121 | commit_parse_limit = 25 |
|
128 | commit_parse_limit = 25 | |
@@ -259,15 +266,8 b' use_celery = false' | |||||
259 | ## Example: use the message queue on the local virtual host 'kallitheavhost' as the RabbitMQ user 'kallithea': |
|
266 | ## Example: use the message queue on the local virtual host 'kallitheavhost' as the RabbitMQ user 'kallithea': | |
260 | celery.broker_url = amqp://kallithea:thepassword@localhost:5672/kallitheavhost |
|
267 | celery.broker_url = amqp://kallithea:thepassword@localhost:5672/kallitheavhost | |
261 |
|
268 | |||
262 | celery.result_backend = db+sqlite:///celery-results.db |
|
|||
263 |
|
||||
264 | #celery.amqp.task.result.expires = 18000 |
|
|||
265 |
|
||||
266 | celery.worker_concurrency = 2 |
|
269 | celery.worker_concurrency = 2 | |
267 | celery.worker_max_tasks_per_child = 1 |
|
270 | celery.worker_max_tasks_per_child = 100 | |
268 |
|
||||
269 | ## If true, tasks will never be sent to the queue, but executed locally instead. |
|
|||
270 | celery.task_always_eager = false |
|
|||
271 |
|
271 | |||
272 | #################################### |
|
272 | #################################### | |
273 | ## BEAKER CACHE ## |
|
273 | ## BEAKER CACHE ## | |
@@ -346,7 +346,6 b' get trace_errors.smtp_username = smtp_us' | |||||
346 | get trace_errors.smtp_password = smtp_password |
|
346 | get trace_errors.smtp_password = smtp_password | |
347 | get trace_errors.smtp_use_tls = smtp_use_tls |
|
347 | get trace_errors.smtp_use_tls = smtp_use_tls | |
348 |
|
348 | |||
349 |
|
||||
350 | ################################## |
|
349 | ################################## | |
351 | ## LOGVIEW CONFIG ## |
|
350 | ## LOGVIEW CONFIG ## | |
352 | ################################## |
|
351 | ################################## | |
@@ -359,10 +358,10 b' logview.pylons.util = #eee' | |||||
359 | ## DB CONFIG ## |
|
358 | ## DB CONFIG ## | |
360 | ######################### |
|
359 | ######################### | |
361 |
|
360 | |||
362 | ## SQLITE [default] |
|
|||
363 | sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60 |
|
361 | sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60 | |
364 |
|
362 | #sqlalchemy.url = postgresql://kallithea:password@localhost/kallithea | ||
365 | ## see sqlalchemy docs for other backends |
|
363 | #sqlalchemy.url = mysql://kallithea:password@localhost/kallithea?charset=utf8mb4 | |
|
364 | ## Note: the mysql:// prefix should also be used for MariaDB | |||
366 |
|
365 | |||
367 | sqlalchemy.pool_recycle = 3600 |
|
366 | sqlalchemy.pool_recycle = 3600 | |
368 |
|
367 |
@@ -14,7 +14,7 b'' | |||||
14 | import os |
|
14 | import os | |
15 | import sys |
|
15 | import sys | |
16 |
|
16 | |||
17 | from kallithea import __version__ |
|
17 | import kallithea | |
18 |
|
18 | |||
19 |
|
19 | |||
20 | # If extensions (or modules to document with autodoc) are in another directory, |
|
20 | # If extensions (or modules to document with autodoc) are in another directory, | |
@@ -47,7 +47,7 b" master_doc = 'index'" | |||||
47 |
|
47 | |||
48 | # General information about the project. |
|
48 | # General information about the project. | |
49 | project = 'Kallithea' |
|
49 | project = 'Kallithea' | |
50 |
copyright = '2010-202 |
|
50 | copyright = '2010-2021 by various authors, licensed as GPLv3.' | |
51 |
|
51 | |||
52 | # The version info for the project you're documenting, acts as replacement for |
|
52 | # The version info for the project you're documenting, acts as replacement for | |
53 | # |version| and |release|, also used in various other places throughout the |
|
53 | # |version| and |release|, also used in various other places throughout the | |
@@ -56,9 +56,9 b" copyright = '2010-2020 by various author" | |||||
56 | # The short X.Y version. |
|
56 | # The short X.Y version. | |
57 | root = os.path.dirname(os.path.dirname(__file__)) |
|
57 | root = os.path.dirname(os.path.dirname(__file__)) | |
58 | sys.path.append(root) |
|
58 | sys.path.append(root) | |
59 | version = __version__ |
|
59 | version = kallithea.__version__ | |
60 | # The full version, including alpha/beta/rc tags. |
|
60 | # The full version, including alpha/beta/rc tags. | |
61 | release = __version__ |
|
61 | release = kallithea.__version__ | |
62 |
|
62 | |||
63 | # The language for content autogenerated by Sphinx. Refer to documentation |
|
63 | # The language for content autogenerated by Sphinx. Refer to documentation | |
64 | # for a list of supported languages. |
|
64 | # for a list of supported languages. |
@@ -26,12 +26,13 b' for more details.' | |||||
26 | Getting started |
|
26 | Getting started | |
27 | --------------- |
|
27 | --------------- | |
28 |
|
28 | |||
29 |
To get started with Kallithea development |
|
29 | To get started with Kallithea development run the following commands in your | |
|
30 | bash shell:: | |||
30 |
|
31 | |||
31 | hg clone https://kallithea-scm.org/repos/kallithea |
|
32 | hg clone https://kallithea-scm.org/repos/kallithea | |
32 | cd kallithea |
|
33 | cd kallithea | |
33 |
python3 -m venv |
|
34 | python3 -m venv venv | |
34 |
|
|
35 | . venv/bin/activate | |
35 | pip install --upgrade pip setuptools |
|
36 | pip install --upgrade pip setuptools | |
36 | pip install --upgrade -e . -r dev_requirements.txt python-ldap python-pam |
|
37 | pip install --upgrade -e . -r dev_requirements.txt python-ldap python-pam | |
37 | kallithea-cli config-create my.ini |
|
38 | kallithea-cli config-create my.ini | |
@@ -71,6 +72,94 b' review and inclusion, via the mailing li' | |||||
71 | .. _contributing-tests: |
|
72 | .. _contributing-tests: | |
72 |
|
73 | |||
73 |
|
74 | |||
|
75 | Internal dependencies | |||
|
76 | --------------------- | |||
|
77 | ||||
|
78 | We try to keep the code base clean and modular and avoid circular dependencies. | |||
|
79 | Code should only invoke code in layers below itself. | |||
|
80 | ||||
|
81 | Imports should import whole modules ``from`` their parent module, perhaps | |||
|
82 | ``as`` a shortened name. Avoid imports ``from`` modules. | |||
|
83 | ||||
|
84 | To avoid cycles and partially initialized modules, ``__init__.py`` should *not* | |||
|
85 | contain any non-trivial imports. The top level of a module should *not* be a | |||
|
86 | facade for the module functionality. | |||
|
87 | ||||
|
88 | Common code for a module is often in ``base.py``. | |||
|
89 | ||||
|
90 | The important part of the dependency graph is approximately linear. In the | |||
|
91 | following list, modules may only depend on modules below them: | |||
|
92 | ||||
|
93 | ``tests`` | |||
|
94 | Just get the job done - anything goes. | |||
|
95 | ||||
|
96 | ``bin/`` & ``config/`` & ``alembic/`` | |||
|
97 | The main entry points, defined in ``setup.py``. Note: The TurboGears template | |||
|
98 | use ``config`` for the high WSGI application - this is not for low level | |||
|
99 | configuration. | |||
|
100 | ||||
|
101 | ``controllers/`` | |||
|
102 | The top level web application, with TurboGears using the ``root`` controller | |||
|
103 | as entry point, and ``routing`` dispatching to other controllers. | |||
|
104 | ||||
|
105 | ``templates/**.html`` | |||
|
106 | The "view", rendering to HTML. Invoked by controllers which can pass them | |||
|
107 | anything from lower layers - especially ``helpers`` available as ``h`` will | |||
|
108 | cut through all layers, and ``c`` gives access to global variables. | |||
|
109 | ||||
|
110 | ``lib/helpers.py`` | |||
|
111 | High level helpers, exposing everything to templates as ``h``. It depends on | |||
|
112 | everything and has a huge dependency chain, so it should not be used for | |||
|
113 | anything else. TODO. | |||
|
114 | ||||
|
115 | ``controllers/base.py`` | |||
|
116 | The base class of controllers, with lots of model knowledge. | |||
|
117 | ||||
|
118 | ``lib/auth.py`` | |||
|
119 | All things related to authentication. TODO. | |||
|
120 | ||||
|
121 | ``lib/utils.py`` | |||
|
122 | High level utils with lots of model knowledge. TODO. | |||
|
123 | ||||
|
124 | ``lib/hooks.py`` | |||
|
125 | Hooks into "everything" to give centralized logging to database, cache | |||
|
126 | invalidation, and extension handling. TODO. | |||
|
127 | ||||
|
128 | ``model/`` | |||
|
129 | Convenience business logic wrappers around database models. | |||
|
130 | ||||
|
131 | ``model/db.py`` | |||
|
132 | Defines the database schema and provides some additional logic. | |||
|
133 | ||||
|
134 | ``model/scm.py`` | |||
|
135 | All things related to anything. TODO. | |||
|
136 | ||||
|
137 | SQLAlchemy | |||
|
138 | Database session and transaction in thread-local variables. | |||
|
139 | ||||
|
140 | ``lib/utils2.py`` | |||
|
141 | Low level utils specific to Kallithea. | |||
|
142 | ||||
|
143 | ``lib/webutils.py`` | |||
|
144 | Low level generic utils with awareness of the TurboGears environment. | |||
|
145 | ||||
|
146 | TurboGears | |||
|
147 | Request, response and state like i18n gettext in thread-local variables. | |||
|
148 | External dependency with global state - usage should be minimized. | |||
|
149 | ||||
|
150 | ``lib/vcs/`` | |||
|
151 | Previously an independent library. No awareness of web, database, or state. | |||
|
152 | ||||
|
153 | ``lib/*`` | |||
|
154 | Various "pure" functionality not depending on anything else. | |||
|
155 | ||||
|
156 | ``__init__`` | |||
|
157 | Very basic Kallithea constants - some of them are set very early based on ``.ini``. | |||
|
158 | ||||
|
159 | This is not exactly how it is right now, but we aim for something like that. | |||
|
160 | Especially the areas marked as TODO have some problems that need untangling. | |||
|
161 | ||||
|
162 | ||||
74 | Running tests |
|
163 | Running tests | |
75 | ------------- |
|
164 | ------------- | |
76 |
|
165 | |||
@@ -84,6 +173,17 b' Note that on unix systems, the temporary' | |||||
84 | and the test suite creates repositories in the temporary directory. Linux |
|
173 | and the test suite creates repositories in the temporary directory. Linux | |
85 | systems with /tmp mounted noexec will thus fail. |
|
174 | systems with /tmp mounted noexec will thus fail. | |
86 |
|
175 | |||
|
176 | Tests can be run on PostgreSQL like:: | |||
|
177 | ||||
|
178 | sudo -u postgres createuser 'kallithea-test' --pwprompt # password password | |||
|
179 | sudo -u postgres createdb 'kallithea-test' --owner 'kallithea-test' | |||
|
180 | REUSE_TEST_DB='postgresql://kallithea-test:password@localhost/kallithea-test' py.test | |||
|
181 | ||||
|
182 | Tests can be run on MariaDB/MySQL like:: | |||
|
183 | ||||
|
184 | echo "GRANT ALL PRIVILEGES ON \`kallithea-test\`.* TO 'kallithea-test'@'localhost' IDENTIFIED BY 'password'" | sudo -u mysql mysql | |||
|
185 | TEST_DB='mysql://kallithea-test:password@localhost/kallithea-test?charset=utf8mb4' py.test | |||
|
186 | ||||
87 | You can also use ``tox`` to run the tests with all supported Python versions. |
|
187 | You can also use ``tox`` to run the tests with all supported Python versions. | |
88 |
|
188 | |||
89 | When running tests, Kallithea generates a `test.ini` based on template values |
|
189 | When running tests, Kallithea generates a `test.ini` based on template values | |
@@ -147,8 +247,9 b' committer/contributor and under GPLv3 un' | |||||
147 | lot about preservation of copyright and license information for existing code |
|
247 | lot about preservation of copyright and license information for existing code | |
148 | that is brought into the project. |
|
248 | that is brought into the project. | |
149 |
|
249 | |||
150 |
Contributions will be accepted in most formats -- such as commits hosted on your |
|
250 | Contributions will be accepted in most formats -- such as commits hosted on your | |
151 | email to the `kallithea-general`_ mailing list. |
|
251 | own Kallithea instance, or patches sent by email to the `kallithea-general`_ | |
|
252 | mailing list. | |||
152 |
|
253 | |||
153 | Make sure to test your changes both manually and with the automatic tests |
|
254 | Make sure to test your changes both manually and with the automatic tests | |
154 | before posting. |
|
255 | before posting. |
@@ -81,7 +81,6 b' Developer guide' | |||||
81 | .. _python: http://www.python.org/ |
|
81 | .. _python: http://www.python.org/ | |
82 | .. _django: http://www.djangoproject.com/ |
|
82 | .. _django: http://www.djangoproject.com/ | |
83 | .. _mercurial: https://www.mercurial-scm.org/ |
|
83 | .. _mercurial: https://www.mercurial-scm.org/ | |
84 | .. _subversion: http://subversion.tigris.org/ |
|
|||
85 | .. _git: http://git-scm.com/ |
|
84 | .. _git: http://git-scm.com/ | |
86 | .. _celery: http://celeryproject.org/ |
|
85 | .. _celery: http://celeryproject.org/ | |
87 | .. _Sphinx: http://sphinx.pocoo.org/ |
|
86 | .. _Sphinx: http://sphinx.pocoo.org/ |
@@ -19,12 +19,12 b' The following describes three different ' | |||||
19 | installations side by side or remove it entirely by just removing the |
|
19 | installations side by side or remove it entirely by just removing the | |
20 | virtualenv directory) and does not require root privileges. |
|
20 | virtualenv directory) and does not require root privileges. | |
21 |
|
21 | |||
22 | - :ref:`installation-without-virtualenv`: The alternative method of installing |
|
22 | - Kallithea can also be installed with plain pip - globally or with ``--user`` | |
23 | a Kallithea release is using standard pip. The package will be installed in |
|
23 | or similar. The package will be installed in the same location as all other | |
24 |
|
|
24 | Python packages you have ever installed. As a result, removing it is not as | |
25 |
|
|
25 | straightforward as with a virtualenv, as you'd have to remove its | |
26 |
|
|
26 | dependencies manually and make sure that they are not needed by other | |
27 | needed by other packages. |
|
27 | packages. We recommend using virtualenv. | |
28 |
|
28 | |||
29 | Regardless of the installation method you may need to make sure you have |
|
29 | Regardless of the installation method you may need to make sure you have | |
30 | appropriate development packages installed, as installation of some of the |
|
30 | appropriate development packages installed, as installation of some of the | |
@@ -49,17 +49,24 b' Installation from repository source' | |||||
49 | ----------------------------------- |
|
49 | ----------------------------------- | |
50 |
|
50 | |||
51 | To install Kallithea in a virtualenv using the stable branch of the development |
|
51 | To install Kallithea in a virtualenv using the stable branch of the development | |
52 | repository, follow the instructions below:: |
|
52 | repository, use the following commands in your bash shell:: | |
53 |
|
53 | |||
54 | hg clone https://kallithea-scm.org/repos/kallithea -u stable |
|
54 | hg clone https://kallithea-scm.org/repos/kallithea -u stable | |
55 | cd kallithea |
|
55 | cd kallithea | |
56 |
python3 -m venv |
|
56 | python3 -m venv venv | |
57 |
. |
|
57 | . venv/bin/activate | |
58 | pip install --upgrade pip setuptools |
|
58 | pip install --upgrade pip setuptools | |
59 | pip install --upgrade -e . |
|
59 | pip install --upgrade -e . | |
60 | python3 setup.py compile_catalog # for translation of the UI |
|
60 | python3 setup.py compile_catalog # for translation of the UI | |
61 |
|
61 | |||
62 | You can now proceed to :ref:`setup`. |
|
62 | .. note:: | |
|
63 | This will install all Python dependencies into the virtualenv. Kallithea | |||
|
64 | itself will however only be installed as a pointer to the source location. | |||
|
65 | The source clone must thus be kept in the same location, and it shouldn't be | |||
|
66 | updated to other revisions unless you want to upgrade. Edits in the source | |||
|
67 | tree will have immediate impact (possibly after a restart of the service). | |||
|
68 | ||||
|
69 | You can now proceed to :ref:`prepare-front-end-files`. | |||
63 |
|
70 | |||
64 | .. _installation-virtualenv: |
|
71 | .. _installation-virtualenv: | |
65 |
|
72 | |||
@@ -73,27 +80,30 b' main Python installation and other appli' | |||||
73 | problematic when upgrading the system or Kallithea. |
|
80 | problematic when upgrading the system or Kallithea. | |
74 | An additional benefit of virtualenv is that it doesn't require root privileges. |
|
81 | An additional benefit of virtualenv is that it doesn't require root privileges. | |
75 |
|
82 | |||
76 | - Assuming you have installed virtualenv, create a new virtual environment |
|
83 | - Don't install as root - install as a dedicated user like ``kallithea``. | |
77 | for example, in `/srv/kallithea/venv`, using the venv command:: |
|
84 | If necessary, create the top directory for the virtualenv (like | |
|
85 | ``/srv/kallithea/venv``) as root and assign ownership to the user. | |||
|
86 | ||||
|
87 | Make a parent folder for the virtualenv (and perhaps also Kallithea | |||
|
88 | configuration and data files) such as ``/srv/kallithea``. Create the | |||
|
89 | directory as root if necessary and grant ownership to the ``kallithea`` user. | |||
|
90 | ||||
|
91 | - Create a new virtual environment, for example in ``/srv/kallithea/venv``, | |||
|
92 | specifying the right Python binary:: | |||
78 |
|
93 | |||
79 | python3 -m venv /srv/kallithea/venv |
|
94 | python3 -m venv /srv/kallithea/venv | |
80 |
|
95 | |||
81 | - Activate the virtualenv in your current shell session and make sure the |
|
96 | - Activate the virtualenv in your current shell session and make sure the | |
82 |
basic requirements are up-to-date by running |
|
97 | basic requirements are up-to-date by running the following commands in your | |
|
98 | bash shell:: | |||
83 |
|
99 | |||
84 | . /srv/kallithea/venv/bin/activate |
|
100 | . /srv/kallithea/venv/bin/activate | |
85 | pip install --upgrade pip setuptools |
|
101 | pip install --upgrade pip setuptools | |
86 |
|
102 | |||
87 |
.. note:: You can't use UNIX ``sudo`` to source the `` |
|
103 | .. note:: You can't use UNIX ``sudo`` to source the ``activate`` script; it | |
88 |
will "activate" a shell that terminates immediately. |
|
104 | will "activate" a shell that terminates immediately. | |
89 | acceptable (and desirable) to create a virtualenv as a normal user. |
|
|||
90 |
|
105 | |||
91 | - Make a folder for Kallithea data files, and configuration somewhere on the |
|
106 | - Install Kallithea in the activated virtualenv:: | |
92 | filesystem. For example:: |
|
|||
93 |
|
||||
94 | mkdir /srv/kallithea |
|
|||
95 |
|
||||
96 | - Go into the created directory and run this command to install Kallithea:: |
|
|||
97 |
|
107 | |||
98 | pip install --upgrade kallithea |
|
108 | pip install --upgrade kallithea | |
99 |
|
109 | |||
@@ -105,31 +115,30 b' An additional benefit of virtualenv is t' | |||||
105 | This might require installation of development packages using your |
|
115 | This might require installation of development packages using your | |
106 | distribution's package manager. |
|
116 | distribution's package manager. | |
107 |
|
117 | |||
108 | Alternatively, download a .tar.gz from http://pypi.python.org/pypi/Kallithea, |
|
118 | Alternatively, download a .tar.gz from http://pypi.python.org/pypi/Kallithea, | |
109 | extract it and install from source by running:: |
|
119 | extract it and install from source by running:: | |
110 |
|
120 | |||
111 | pip install --upgrade . |
|
121 | pip install --upgrade . | |
112 |
|
122 | |||
113 | - This will install Kallithea together with all other required |
|
123 | - This will install Kallithea together with all other required | |
114 | Python libraries into the activated virtualenv. |
|
124 | Python libraries into the activated virtualenv. | |
115 |
|
125 | |||
116 |
You can now proceed to :ref:` |
|
126 | You can now proceed to :ref:`prepare-front-end-files`. | |
117 |
|
127 | |||
118 | .. _installation-without-virtualenv: |
|
128 | .. _prepare-front-end-files: | |
119 |
|
129 | |||
120 |
|
130 | |||
121 | Installing a released version without virtualenv |
|
131 | Prepare front-end files | |
122 |
----------------------- |
|
132 | ----------------------- | |
123 |
|
||||
124 | For installation without virtualenv, 'just' use:: |
|
|||
125 |
|
||||
126 | pip install kallithea |
|
|||
127 |
|
133 | |||
128 | Note that this method requires root privileges and will install packages |
|
134 | Finally, the front-end files with CSS and JavaScript must be prepared. This | |
129 | globally without using the system's package manager. |
|
135 | depends on having some commands available in the shell search path: ``npm`` | |
|
136 | version 6 or later, and ``node.js`` (version 12 or later) available as | |||
|
137 | ``node``. The installation method for these dependencies varies between | |||
|
138 | operating systems and distributions. | |||
130 |
|
139 | |||
131 | To install as a regular user in ``~/.local``, you can use:: |
|
140 | Prepare the front-end by running:: | |
132 |
|
141 | |||
133 | pip install --user kallithea |
|
142 | kallithea-cli front-end-build | |
134 |
|
143 | |||
135 | You can now proceed to :ref:`setup`. |
|
144 | You can now proceed to :ref:`setup`. |
@@ -20,23 +20,27 b' 1. **Prepare environment and external de' | |||||
20 | 2. **Install Kallithea software.** |
|
20 | 2. **Install Kallithea software.** | |
21 | This makes the ``kallithea-cli`` command line tool available. |
|
21 | This makes the ``kallithea-cli`` command line tool available. | |
22 |
|
22 | |||
23 | 3. **Create low level configuration file.** |
|
23 | 3. **Prepare front-end files** | |
|
24 | Some front-end files must be fetched or created using ``npm`` and ``node`` | |||
|
25 | tooling so they can be served to the client as static files. | |||
|
26 | ||||
|
27 | 4. **Create low level configuration file.** | |||
24 | Use ``kallithea-cli config-create`` to create a ``.ini`` file with database |
|
28 | Use ``kallithea-cli config-create`` to create a ``.ini`` file with database | |
25 | connection info, mail server information, configuration for the specified |
|
29 | connection info, mail server information, configuration for the specified | |
26 | web server, etc. |
|
30 | web server, etc. | |
27 |
|
31 | |||
28 |
|
|
32 | 5. **Populate the database.** | |
29 | Use ``kallithea-cli db-create`` with the ``.ini`` file to create the |
|
33 | Use ``kallithea-cli db-create`` with the ``.ini`` file to create the | |
30 | database schema and insert the most basic information: the location of the |
|
34 | database schema and insert the most basic information: the location of the | |
31 | repository store and an initial local admin user. |
|
35 | repository store and an initial local admin user. | |
32 |
|
36 | |||
33 |
|
|
37 | 6. **Configure the web server.** | |
34 | The web server must invoke the WSGI entrypoint for the Kallithea software |
|
38 | The web server must invoke the WSGI entrypoint for the Kallithea software | |
35 | using the ``.ini`` file (and thus the database). This makes the web |
|
39 | using the ``.ini`` file (and thus the database). This makes the web | |
36 | application available so the local admin user can log in and tweak the |
|
40 | application available so the local admin user can log in and tweak the | |
37 | configuration further. |
|
41 | configuration further. | |
38 |
|
42 | |||
39 |
|
|
43 | 7. **Configure users.** | |
40 | The initial admin user can create additional local users, or configure how |
|
44 | The initial admin user can create additional local users, or configure how | |
41 | users can be created and authenticated from other user directories. |
|
45 | users can be created and authenticated from other user directories. | |
42 |
|
46 | |||
@@ -44,6 +48,45 b' See the subsequent sections, the separat' | |||||
44 | :ref:`setup` for details on these steps. |
|
48 | :ref:`setup` for details on these steps. | |
45 |
|
49 | |||
46 |
|
50 | |||
|
51 | File system location | |||
|
52 | -------------------- | |||
|
53 | ||||
|
54 | Kallithea can be installed in many different ways. The main parts are: | |||
|
55 | ||||
|
56 | - A location for the Kallithea software and its dependencies. This includes | |||
|
57 | the Python code, template files, and front-end code. After installation, this | |||
|
58 | will be read-only (except when upgrading). | |||
|
59 | ||||
|
60 | - A location for the ``.ini`` configuration file that tells the Kallithea | |||
|
61 | instance which database to use (and thus also the repository location). | |||
|
62 | After installation, this will be read-only (except when upgrading). | |||
|
63 | ||||
|
64 | - A location for various data files and caches for the Kallithea instance. This | |||
|
65 | is by default in a ``data`` directory next to the ``.ini`` file. This will | |||
|
66 | have to be writable by the running Kallithea service. | |||
|
67 | ||||
|
68 | - A database. The ``.ini`` file specifies which database to use. The database | |||
|
69 | will be a separate service and live elsewhere in the filesystem if using | |||
|
70 | PostgreSQL or MariaDB/MySQL. If using SQLite, it will by default live next to | |||
|
71 | the ``.ini`` file, as ``kallithea.db``. | |||
|
72 | ||||
|
73 | - A location for the repositories that are hosted by this Kallithea instance. | |||
|
74 | This will have to be writable by the running Kallithea service. The path to | |||
|
75 | this location will be configured in the database. | |||
|
76 | ||||
|
77 | For production setups, one recommendation is to use ``/srv/kallithea`` for the | |||
|
78 | ``.ini`` and ``data``, place the virtualenv in ``venv``, and use a Kallithea | |||
|
79 | clone in ``kallithea``. Create a ``kallithea`` user, let it own | |||
|
80 | ``/srv/kallithea``, and run as that user when installing. | |||
|
81 | ||||
|
82 | For simple setups, it is fine to just use something like a ``kallithea`` user | |||
|
83 | with home in ``/home/kallithea`` and place everything there. | |||
|
84 | ||||
|
85 | For experiments, it might be convenient to run everything as yourself and work | |||
|
86 | inside a clone of Kallithea, with the ``.ini`` and SQLite database in the root | |||
|
87 | of the clone, and a virtualenv in ``venv``. | |||
|
88 | ||||
|
89 | ||||
47 | Python environment |
|
90 | Python environment | |
48 | ------------------ |
|
91 | ------------------ | |
49 |
|
92 | |||
@@ -177,7 +220,7 b' There are several web server options:' | |||||
177 | to get a configuration starting point for your choice of web server. |
|
220 | to get a configuration starting point for your choice of web server. | |
178 |
|
221 | |||
179 | (Gearbox will do like ``paste`` and use the WSGI application entry point |
|
222 | (Gearbox will do like ``paste`` and use the WSGI application entry point | |
180 |
``kallithea.config. |
|
223 | ``kallithea.config.application:make_app`` as specified in ``setup.py``.) | |
181 |
|
224 | |||
182 | - `Apache httpd`_ can serve WSGI applications directly using mod_wsgi_ and a |
|
225 | - `Apache httpd`_ can serve WSGI applications directly using mod_wsgi_ and a | |
183 | simple Python file with the necessary configuration. This is a good option if |
|
226 | simple Python file with the necessary configuration. This is a good option if | |
@@ -216,13 +259,13 b' continuous hammering from the internet.' | |||||
216 | .. _Python: http://www.python.org/ |
|
259 | .. _Python: http://www.python.org/ | |
217 | .. _Gunicorn: http://gunicorn.org/ |
|
260 | .. _Gunicorn: http://gunicorn.org/ | |
218 | .. _Gevent: http://www.gevent.org/ |
|
261 | .. _Gevent: http://www.gevent.org/ | |
219 | .. _Waitress: http://waitress.readthedocs.org/en/latest/ |
|
262 | .. _Waitress: https://docs.pylonsproject.org/projects/waitress/ | |
220 | .. _Gearbox: http://turbogears.readthedocs.io/en/latest/turbogears/gearbox.html |
|
263 | .. _Gearbox: https://turbogears.readthedocs.io/en/latest/turbogears/gearbox.html | |
221 | .. _PyPI: https://pypi.python.org/pypi |
|
264 | .. _PyPI: https://pypi.python.org/pypi | |
222 | .. _Apache httpd: http://httpd.apache.org/ |
|
265 | .. _Apache httpd: http://httpd.apache.org/ | |
223 |
.. _mod_wsgi: https:// |
|
266 | .. _mod_wsgi: https://modwsgi.readthedocs.io/ | |
224 | .. _isapi-wsgi: https://github.com/hexdump42/isapi-wsgi |
|
267 | .. _isapi-wsgi: https://github.com/hexdump42/isapi-wsgi | |
225 |
.. _uWSGI: https://uwsgi-docs.readthedocs.o |
|
268 | .. _uWSGI: https://uwsgi-docs.readthedocs.io/ | |
226 | .. _nginx: http://nginx.org/en/ |
|
269 | .. _nginx: http://nginx.org/en/ | |
227 | .. _iis: http://en.wikipedia.org/wiki/Internet_Information_Services |
|
270 | .. _iis: http://en.wikipedia.org/wiki/Internet_Information_Services | |
228 | .. _pip: http://en.wikipedia.org/wiki/Pip_%28package_manager%29 |
|
271 | .. _pip: http://en.wikipedia.org/wiki/Pip_%28package_manager%29 |
@@ -5,35 +5,72 b' Setup' | |||||
5 | ===== |
|
5 | ===== | |
6 |
|
6 | |||
7 |
|
7 | |||
8 | Setting up Kallithea |
|
8 | Setting up a Kallithea instance | |
9 | -------------------- |
|
9 | ------------------------------- | |
|
10 | ||||
|
11 | Some further details to the steps mentioned in the overview. | |||
10 |
|
12 | |||
11 | First, you will need to create a Kallithea configuration file. Run the |
|
13 | Create low level configuration file | |
12 | following command to do so:: |
|
14 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |
|
15 | ||||
|
16 | First, you will need to create a Kallithea configuration file. The | |||
|
17 | configuration file is a ``.ini`` file that contains various low level settings | |||
|
18 | for Kallithea, e.g. configuration of how to use database, web server, email, | |||
|
19 | and logging. | |||
13 |
|
20 | |||
14 | kallithea-cli config-create my.ini |
|
21 | Change to the desired directory (such as ``/srv/kallithea``) as the right user | |
|
22 | and run the following command to create the file ``my.ini`` in the current | |||
|
23 | directory:: | |||
|
24 | ||||
|
25 | kallithea-cli config-create my.ini http_server=waitress | |||
15 |
|
26 | |||
16 | This will create the file ``my.ini`` in the current directory. This |
|
27 | To get a good starting point for your configuration, specify the http server | |
17 | configuration file contains the various settings for Kallithea, e.g. |
|
28 | you intend to use. It can be ``waitress``, ``gearbox``, ``gevent``, | |
18 | proxy port, email settings, usage of static files, cache, Celery |
|
29 | ``gunicorn``, or ``uwsgi``. (Apache ``mod_wsgi`` will not use this | |
19 | settings, and logging. Extra settings can be specified like:: |
|
30 | configuration file, and it is fine to keep the default http_server configuration | |
|
31 | unused. ``mod_wsgi`` is configured using ``httpd.conf`` directives and a WSGI | |||
|
32 | wrapper script.) | |||
|
33 | ||||
|
34 | Extra custom settings can be specified like:: | |||
20 |
|
35 | |||
21 | kallithea-cli config-create my.ini host=8.8.8.8 "[handler_console]" formatter=color_formatter |
|
36 | kallithea-cli config-create my.ini host=8.8.8.8 "[handler_console]" formatter=color_formatter | |
22 |
|
37 | |||
23 | Next, you need to create the databases used by Kallithea. It is recommended to |
|
38 | Populate the database | |
24 | use PostgreSQL or SQLite (default). If you choose a database other than the |
|
39 | ^^^^^^^^^^^^^^^^^^^^^ | |
25 | default, ensure you properly adjust the database URL in your ``my.ini`` |
|
40 | ||
26 | configuration file to use this other database. Kallithea currently supports |
|
41 | Next, you need to create the databases used by Kallithea. Kallithea currently | |
27 |
PostgreSQL, SQLite and MariaDB/MySQL databases. |
|
42 | supports PostgreSQL, SQLite and MariaDB/MySQL databases. It is recommended to | |
28 | the following command:: |
|
43 | start out using SQLite (the default) and move to PostgreSQL if it becomes a | |
|
44 | bottleneck or to get a "proper" database. MariaDB/MySQL is also supported. | |||
|
45 | ||||
|
46 | For PostgreSQL, run ``pip install psycopg2`` to get the database driver. Make | |||
|
47 | sure the PostgreSQL server is initialized and running. Make sure you have a | |||
|
48 | database user with password authentication with permissions to create databases | |||
|
49 | - for example by running:: | |||
|
50 | ||||
|
51 | sudo -u postgres createuser 'kallithea' --pwprompt --createdb | |||
|
52 | ||||
|
53 | For MariaDB/MySQL, run ``pip install mysqlclient`` to get the ``MySQLdb`` | |||
|
54 | database driver. Make sure the database server is initialized and running. Make | |||
|
55 | sure you have a database user with password authentication with permissions to | |||
|
56 | create the database - for example by running:: | |||
|
57 | ||||
|
58 | echo 'CREATE USER "kallithea"@"localhost" IDENTIFIED BY "password"' | sudo -u mysql mysql | |||
|
59 | echo 'GRANT ALL PRIVILEGES ON `kallithea`.* TO "kallithea"@"localhost"' | sudo -u mysql mysql | |||
|
60 | ||||
|
61 | Check and adjust ``sqlalchemy.url`` in your ``my.ini`` configuration file to use | |||
|
62 | this database. | |||
|
63 | ||||
|
64 | Create the database, tables, and initial content by running the following | |||
|
65 | command:: | |||
29 |
|
66 | |||
30 | kallithea-cli db-create -c my.ini |
|
67 | kallithea-cli db-create -c my.ini | |
31 |
|
68 | |||
32 |
This will prompt you for a "root" path. This "root" path is the location |
|
69 | This will first prompt you for a "root" path. This "root" path is the location | |
33 |
Kallithea will store all of its repositories on the current machine. |
|
70 | where Kallithea will store all of its repositories on the current machine. This | |
34 | entering this "root" path ``db-create`` will also prompt you for a username |
|
71 | location must be writable for the running Kallithea application. Next, | |
35 | and password for the initial admin account which ``db-create`` sets |
|
72 | ``db-create`` will prompt you for a username and password for the initial admin | |
36 | up for you. |
|
73 | account it sets up for you. | |
37 |
|
74 | |||
38 | The ``db-create`` values can also be given on the command line. |
|
75 | The ``db-create`` values can also be given on the command line. | |
39 | Example:: |
|
76 | Example:: | |
@@ -48,19 +85,20 b' repositories Kallithea will add all of t' | |||||
48 | location to its database. (Note: make sure you specify the correct |
|
85 | location to its database. (Note: make sure you specify the correct | |
49 | path to the root). |
|
86 | path to the root). | |
50 |
|
87 | |||
51 | .. note:: the given path for Mercurial_ repositories **must** be write |
|
88 | .. note:: It is also possible to use an existing database. For example, | |
52 | accessible for the application. It's very important since |
|
89 | when using PostgreSQL without granting general createdb privileges to | |
53 | the Kallithea web interface will work without write access, |
|
90 | the PostgreSQL kallithea user, set ``sqlalchemy.url = | |
54 | but when trying to do a push it will fail with permission |
|
91 | postgresql://kallithea:password@localhost/kallithea`` and create the | |
55 | denied errors unless it has write access. |
|
92 | database like:: | |
56 |
|
|
93 | ||
57 | Finally, the front-end files must be prepared. This requires ``npm`` version 6 |
|
94 | sudo -u postgres createdb 'kallithea' --owner 'kallithea' | |
58 | or later, which needs ``node.js`` (version 12 or later). Prepare the front-end |
|
95 | kallithea-cli db-create -c my.ini --reuse | |
59 | by running:: |
|
|||
60 |
|
96 | |||
61 | kallithea-cli front-end-build |
|
97 | Running | |
|
98 | ^^^^^^^ | |||
62 |
|
99 | |||
63 |
You are now ready to use Kallithea. To run it |
|
100 | You are now ready to use Kallithea. To run it using a gearbox web server, | |
|
101 | simply execute:: | |||
64 |
|
102 | |||
65 | gearbox serve -c my.ini |
|
103 | gearbox serve -c my.ini | |
66 |
|
104 | |||
@@ -186,7 +224,7 b' Setting up Whoosh full text search' | |||||
186 |
|
224 | |||
187 | Kallithea provides full text search of repositories using `Whoosh`__. |
|
225 | Kallithea provides full text search of repositories using `Whoosh`__. | |
188 |
|
226 | |||
189 |
.. __: https://whoosh.readthedocs.io/ |
|
227 | .. __: https://whoosh.readthedocs.io/ | |
190 |
|
228 | |||
191 | For an incremental index build, run:: |
|
229 | For an incremental index build, run:: | |
192 |
|
230 | |||
@@ -300,15 +338,21 b' the supported syntax in ``issue_pat``, `' | |||||
300 | Hook management |
|
338 | Hook management | |
301 | --------------- |
|
339 | --------------- | |
302 |
|
340 | |||
303 |
|
|
341 | Custom Mercurial hooks can be managed in a similar way to that used in ``.hgrc`` files. | |
304 | To manage hooks, choose *Admin > Settings > Hooks*. |
|
342 | To manage hooks, choose *Admin > Settings > Hooks*. | |
305 |
|
343 | |||
306 | The built-in hooks cannot be modified, though they can be enabled or disabled in the *VCS* section. |
|
|||
307 |
|
||||
308 | To add another custom hook simply fill in the first textbox with |
|
344 | To add another custom hook simply fill in the first textbox with | |
309 | ``<name>.<hook_type>`` and the second with the hook path. Example hooks |
|
345 | ``<name>.<hook_type>`` and the second with the hook path. Example hooks | |
310 | can be found in ``kallithea.lib.hooks``. |
|
346 | can be found in ``kallithea.lib.hooks``. | |
311 |
|
347 | |||
|
348 | Kallithea will also use some hooks internally. They cannot be modified, but | |||
|
349 | some of them can be enabled or disabled in the *VCS* section. | |||
|
350 | ||||
|
351 | Kallithea does not actively support custom Git hooks, but hooks can be installed | |||
|
352 | manually in the file system. Kallithea will install and use the | |||
|
353 | ``post-receive`` Git hook internally, but it will then invoke | |||
|
354 | ``post-receive-custom`` if present. | |||
|
355 | ||||
312 |
|
356 | |||
313 | Changing default encoding |
|
357 | Changing default encoding | |
314 | ------------------------- |
|
358 | ------------------------- | |
@@ -362,6 +406,38 b' for more info.' | |||||
362 | user that Kallithea runs. |
|
406 | user that Kallithea runs. | |
363 |
|
407 | |||
364 |
|
408 | |||
|
409 | Proxy setups | |||
|
410 | ------------ | |||
|
411 | ||||
|
412 | When Kallithea is processing HTTP requests from a user, it will see and use | |||
|
413 | some of the basic properties of the connection, both at the TCP/IP level and at | |||
|
414 | the HTTP level. The WSGI server will provide this information to Kallithea in | |||
|
415 | the "environment". | |||
|
416 | ||||
|
417 | In some setups, a proxy server will take requests from users and forward | |||
|
418 | them to the actual Kallithea server. The proxy server will thus be the | |||
|
419 | immediate client of the Kallithea WSGI server, and Kallithea will basically see | |||
|
420 | it as such. To make sure Kallithea sees the request as it arrived from the | |||
|
421 | client to the proxy server, the proxy server must be configured to | |||
|
422 | somehow pass the original information on to Kallithea, and Kallithea must be | |||
|
423 | configured to pick that information up and trust it. | |||
|
424 | ||||
|
425 | Kallithea will by default rely on its WSGI server to provide the IP of the | |||
|
426 | client in the WSGI environment as ``REMOTE_ADDR``, but it can be configured to | |||
|
427 | get it from an HTTP header that has been set by the proxy server. For | |||
|
428 | example, if the proxy server puts the client IP in the ``X-Forwarded-For`` | |||
|
429 | HTTP header, set:: | |||
|
430 | ||||
|
431 | remote_addr_variable = HTTP_X_FORWARDED_FOR | |||
|
432 | ||||
|
433 | Kallithea will by default rely on finding the protocol (``http`` or ``https``) | |||
|
434 | in the WSGI environment as ``wsgi.url_scheme``. If the proxy server puts | |||
|
435 | the protocol of the client request in the ``X-Forwarded-Proto`` HTTP header, | |||
|
436 | Kallithea can be configured to trust that header by setting:: | |||
|
437 | ||||
|
438 | url_scheme_variable = HTTP_X_FORWARDED_PROTO | |||
|
439 | ||||
|
440 | ||||
365 | HTTPS support |
|
441 | HTTPS support | |
366 | ------------- |
|
442 | ------------- | |
367 |
|
443 | |||
@@ -370,10 +446,9 b' Kallithea will by default generate URLs ' | |||||
370 | Alternatively, you can use some special configuration settings to control |
|
446 | Alternatively, you can use some special configuration settings to control | |
371 | directly which scheme/protocol Kallithea will use when generating URLs: |
|
447 | directly which scheme/protocol Kallithea will use when generating URLs: | |
372 |
|
448 | |||
373 |
- With `` |
|
449 | - With ``url_scheme_variable`` set, the scheme will be taken from that HTTP | |
374 | ``X-Url-Scheme``, ``X-Forwarded-Scheme`` or ``X-Forwarded-Proto`` HTTP header |
|
450 | header. | |
375 | (default ``http``). |
|
451 | - With ``force_https = true``, the scheme will be seen as ``https``. | |
376 | - With ``force_https = true`` the default will be ``https``. |
|
|||
377 | - With ``use_htsts = true``, Kallithea will set ``Strict-Transport-Security`` when using https. |
|
452 | - With ``use_htsts = true``, Kallithea will set ``Strict-Transport-Security`` when using https. | |
378 |
|
453 | |||
379 | .. _nginx_virtual_host: |
|
454 | .. _nginx_virtual_host: | |
@@ -556,43 +631,19 b" that, you'll need to:" | |||||
556 |
|
631 | |||
557 | WSGIRestrictEmbedded On |
|
632 | WSGIRestrictEmbedded On | |
558 |
|
633 | |||
559 |
- Create a WSGI dispatch script, like the one below. |
|
634 | - Create a WSGI dispatch script, like the one below. The ``WSGIDaemonProcess`` | |
560 | check that the paths correctly point to where you installed Kallithea |
|
635 | ``python-home`` directive will make sure it uses the right Python Virtual | |
561 | and its Python Virtual Environment. |
|
636 | Environment and that paste thus can pick up the right Kallithea | |
|
637 | application. | |||
562 |
|
638 | |||
563 | .. code-block:: python |
|
639 | .. code-block:: python | |
564 |
|
640 | |||
565 | import os |
|
|||
566 | os.environ['PYTHON_EGG_CACHE'] = '/srv/kallithea/.egg-cache' |
|
|||
567 |
|
||||
568 | # sometimes it's needed to set the current dir |
|
|||
569 | os.chdir('/srv/kallithea/') |
|
|||
570 |
|
||||
571 | import site |
|
|||
572 | site.addsitedir("/srv/kallithea/venv/lib/python3.7/site-packages") |
|
|||
573 |
|
||||
574 | ini = '/srv/kallithea/my.ini' |
|
641 | ini = '/srv/kallithea/my.ini' | |
575 | from logging.config import fileConfig |
|
642 | from logging.config import fileConfig | |
576 | fileConfig(ini, {'__file__': ini, 'here': '/srv/kallithea'}) |
|
643 | fileConfig(ini, {'__file__': ini, 'here': '/srv/kallithea'}) | |
577 | from paste.deploy import loadapp |
|
644 | from paste.deploy import loadapp | |
578 | application = loadapp('config:' + ini) |
|
645 | application = loadapp('config:' + ini) | |
579 |
|
646 | |||
580 | Or using proper virtualenv activation: |
|
|||
581 |
|
||||
582 | .. code-block:: python |
|
|||
583 |
|
||||
584 | activate_this = '/srv/kallithea/venv/bin/activate_this.py' |
|
|||
585 | execfile(activate_this, dict(__file__=activate_this)) |
|
|||
586 |
|
||||
587 | import os |
|
|||
588 | os.environ['HOME'] = '/srv/kallithea' |
|
|||
589 |
|
||||
590 | ini = '/srv/kallithea/kallithea.ini' |
|
|||
591 | from logging.config import fileConfig |
|
|||
592 | fileConfig(ini, {'__file__': ini, 'here': '/srv/kallithea'}) |
|
|||
593 | from paste.deploy import loadapp |
|
|||
594 | application = loadapp('config:' + ini) |
|
|||
595 |
|
||||
596 | - Add the necessary ``WSGI*`` directives to the Apache Virtual Host configuration |
|
647 | - Add the necessary ``WSGI*`` directives to the Apache Virtual Host configuration | |
597 | file, like in the example below. Notice that the WSGI dispatch script created |
|
648 | file, like in the example below. Notice that the WSGI dispatch script created | |
598 | above is referred to with the ``WSGIScriptAlias`` directive. |
|
649 | above is referred to with the ``WSGIScriptAlias`` directive. | |
@@ -617,15 +668,6 b" that, you'll need to:" | |||||
617 | WSGIScriptAlias / /srv/kallithea/dispatch.wsgi |
|
668 | WSGIScriptAlias / /srv/kallithea/dispatch.wsgi | |
618 | WSGIPassAuthorization On |
|
669 | WSGIPassAuthorization On | |
619 |
|
670 | |||
620 | Or if using a dispatcher WSGI script with proper virtualenv activation: |
|
|||
621 |
|
||||
622 | .. code-block:: apache |
|
|||
623 |
|
||||
624 | WSGIDaemonProcess kallithea processes=5 threads=1 maximum-requests=100 lang=en_US.utf8 |
|
|||
625 | WSGIProcessGroup kallithea |
|
|||
626 | WSGIScriptAlias / /srv/kallithea/dispatch.wsgi |
|
|||
627 | WSGIPassAuthorization On |
|
|||
628 |
|
||||
629 |
|
671 | |||
630 | Other configuration files |
|
672 | Other configuration files | |
631 | ------------------------- |
|
673 | ------------------------- |
@@ -39,8 +39,8 b' Back up your configuration' | |||||
39 |
|
39 | |||
40 | Make a copy of your Kallithea configuration (``.ini``) file. |
|
40 | Make a copy of your Kallithea configuration (``.ini``) file. | |
41 |
|
41 | |||
42 |
If you are using :ref:` |
|
42 | If you are using custom :ref:`extensions <customization>`, you should also | |
43 |
make a copy of the |
|
43 | make a copy of the ``extensions.py`` file. | |
44 |
|
44 | |||
45 | Back up your database |
|
45 | Back up your database | |
46 | ^^^^^^^^^^^^^^^^^^^^^ |
|
46 | ^^^^^^^^^^^^^^^^^^^^^ | |
@@ -225,14 +225,21 b' clear out your log file so that new erro' | |||||
225 | upgrade. |
|
225 | upgrade. | |
226 |
|
226 | |||
227 |
|
227 | |||
228 |
10. |
|
228 | 10. Reinstall internal Git repository hooks | |
229 | ------------------------------- |
|
229 | ------------------------------------------- | |
230 |
|
230 | |||
231 | It is possible that an upgrade involves changes to the Git hooks installed by |
|
231 | It is possible that an upgrade involves changes to the Git hooks installed by | |
232 | Kallithea. As these hooks are created inside the repositories on the server |
|
232 | Kallithea. As these hooks are created inside the repositories on the server | |
233 | filesystem, they are not updated automatically when upgrading Kallithea itself. |
|
233 | filesystem, they are not updated automatically when upgrading Kallithea itself. | |
234 |
|
234 | |||
235 | To update the hooks of your Git repositories: |
|
235 | To update the hooks of your Git repositories, run:: | |
|
236 | ||||
|
237 | kallithea-cli repo-scan -c my.ini --install-git-hooks | |||
|
238 | ||||
|
239 | Watch out for warnings like ``skipping overwriting hook file X``, then fix it | |||
|
240 | and rerun, or consider using ``--overwrite-git-hooks`` instead. | |||
|
241 | ||||
|
242 | Or: | |||
236 |
|
243 | |||
237 | * Go to *Admin > Settings > Remap and Rescan* |
|
244 | * Go to *Admin > Settings > Remap and Rescan* | |
238 | * Select the checkbox *Install Git hooks* |
|
245 | * Select the checkbox *Install Git hooks* |
@@ -39,13 +39,14 b' running::' | |||||
39 | .. _less: http://lesscss.org/ |
|
39 | .. _less: http://lesscss.org/ | |
40 |
|
40 | |||
41 |
|
41 | |||
42 |
Behavioral customization: |
|
42 | Behavioral customization: Kallithea extensions | |
43 | -------------------------------------- |
|
43 | ---------------------------------------------- | |
44 |
|
44 | |||
45 |
Some behavioral customization can be done in Python using |
|
45 | Some behavioral customization can be done in Python using Kallithea | |
46 | custom Python package that can extend Kallithea functionality. |
|
46 | ``extensions``, a custom Python file you can create to extend Kallithea | |
|
47 | functionality. | |||
47 |
|
48 | |||
48 |
With `` |
|
49 | With ``extensions`` it's possible to add additional mappings for Whoosh | |
49 | indexing and statistics, to add additional code into the push/pull/create/delete |
|
50 | indexing and statistics, to add additional code into the push/pull/create/delete | |
50 | repository hooks (for example to send signals to build bots such as Jenkins) and |
|
51 | repository hooks (for example to send signals to build bots such as Jenkins) and | |
51 | even to monkey-patch certain parts of the Kallithea source code (for example |
|
52 | even to monkey-patch certain parts of the Kallithea source code (for example | |
@@ -55,9 +56,14 b' To generate a skeleton extensions packag' | |||||
55 |
|
56 | |||
56 | kallithea-cli extensions-create -c my.ini |
|
57 | kallithea-cli extensions-create -c my.ini | |
57 |
|
58 | |||
58 |
This will create an `` |
|
59 | This will create an ``extensions.py`` file next to the specified ``ini`` file. | |
59 | See the ``__init__.py`` file inside the generated ``rcextensions`` package |
|
60 | You can find more details inside this file. | |
60 | for more details. |
|
61 | ||
|
62 | For compatibility with previous releases of Kallithea, a directory named | |||
|
63 | ``rcextensions`` with a file ``__init__.py`` inside of it can also be used. If | |||
|
64 | both an ``extensions.py`` file and an ``rcextensions`` directory are found, only | |||
|
65 | ``extensions.py`` will be loaded. Note that the name ``rcextensions`` is | |||
|
66 | deprecated and support for it will be removed in a future release. | |||
61 |
|
67 | |||
62 |
|
68 | |||
63 | Behavioral customization: code changes |
|
69 | Behavioral customization: code changes |
@@ -89,8 +89,8 b' a name and an address in the following f' | |||||
89 | References |
|
89 | References | |
90 | ---------- |
|
90 | ---------- | |
91 |
|
91 | |||
92 |
- `Error Middleware (Pylons documentation) <http://pylons-webframework.readthedocs.o |
|
92 | - `Error Middleware (Pylons documentation) <https://pylons-webframework.readthedocs.io/en/latest/debugging.html#error-middleware>`_ | |
93 |
- `ErrorHandler (Pylons modules documentation) <http://pylons-webframework.readthedocs.o |
|
93 | - `ErrorHandler (Pylons modules documentation) <https://pylons-webframework.readthedocs.io/en/latest/modules/middleware.html#pylons.middleware.ErrorHandler>`_ | |
94 |
|
94 | |||
95 |
|
95 | |||
96 | .. _backlash: https://github.com/TurboGears/backlash |
|
96 | .. _backlash: https://github.com/TurboGears/backlash |
@@ -118,22 +118,15 b' Trending source files' | |||||
118 |
|
118 | |||
119 | Trending source files are calculated based on a predefined dictionary of known |
|
119 | Trending source files are calculated based on a predefined dictionary of known | |
120 | types and extensions. If an extension is missing or you would like to scan |
|
120 | types and extensions. If an extension is missing or you would like to scan | |
121 | custom files, it is possible to extend the ``LANGUAGES_EXTENSIONS_MAP`` |
|
121 | custom files, it is possible to add additional file extensions with | |
122 | dictionary located in ``kallithea/config/conf.py`` with new types. |
|
122 | ``EXTRA_MAPPINGS`` in your custom Kallithea extensions.py file. See | |
|
123 | :ref:`customization`. | |||
123 |
|
124 | |||
124 |
|
125 | |||
125 | Cloning remote repositories |
|
126 | Cloning remote repositories | |
126 | --------------------------- |
|
127 | --------------------------- | |
127 |
|
128 | |||
128 | Kallithea has the ability to clone repositories from given remote locations. |
|
129 | Kallithea has the ability to clone repositories from given remote locations. | |
129 | Currently it supports the following options: |
|
|||
130 |
|
||||
131 | - hg -> hg clone |
|
|||
132 | - svn -> hg clone |
|
|||
133 | - git -> git clone |
|
|||
134 |
|
||||
135 | .. note:: svn -> hg cloning requires the ``hgsubversion`` library to be |
|
|||
136 | installed. |
|
|||
137 |
|
130 | |||
138 | If you need to clone repositories that are protected via basic authentication, |
|
131 | If you need to clone repositories that are protected via basic authentication, | |
139 | you can pass the credentials in the URL, e.g. |
|
132 | you can pass the credentials in the URL, e.g. |
@@ -48,42 +48,37 b' database platform.' | |||||
48 | Horizontal scaling |
|
48 | Horizontal scaling | |
49 | ------------------ |
|
49 | ------------------ | |
50 |
|
50 | |||
51 |
Scaling horizontally means running several Kallithea instances |
|
51 | Scaling horizontally means running several Kallithea instances (also known as | |
52 | share the load. That can give huge performance benefits when dealing with large |
|
52 | worker processes) and let them share the load. That is essential to serve other | |
53 | amounts of traffic (many users, CI servers, etc.). Kallithea can be scaled |
|
53 | users while processing a long-running request from a user. Usually, the | |
54 | horizontally on one (recommended) or multiple machines. |
|
54 | bottleneck on a Kallithea server is not CPU but I/O speed - especially network | |
|
55 | speed. It is thus a good idea to run multiple worker processes on one server. | |||
55 |
|
56 | |||
56 | It is generally possible to run WSGI applications multithreaded, so that |
|
57 | .. note:: | |
57 | several HTTP requests are served from the same Python process at once. That can |
|
|||
58 | in principle give better utilization of internal caches and less process |
|
|||
59 | overhead. |
|
|||
60 |
|
58 | |||
61 | One danger of running multithreaded is that program execution becomes much more |
|
59 | Kallithea and the embedded Mercurial backend are not thread-safe. Each | |
62 | complex; programs must be written to consider all combinations of events and |
|
60 | worker process must thus be single-threaded. | |
63 | problems might depend on timing and be impossible to reproduce. |
|
|||
64 |
|
61 | |||
65 | Kallithea can't promise to be thread-safe, just like the embedded Mercurial |
|
62 | Web servers can usually launch multiple worker processes - for example ``mod_wsgi`` with the | |
66 | backend doesn't make any strong promises when used as Kallithea uses it. |
|
63 | ``WSGIDaemonProcess`` ``processes`` parameter or ``uWSGI`` or ``gunicorn`` with | |
67 | Instead, we recommend scaling by using multiple server processes. |
|
64 | their ``workers`` setting. | |
68 |
|
65 | |||
69 | Web servers with multiple worker processes (such as ``mod_wsgi`` with the |
|
66 | Kallithea can also be scaled horizontally across multiple machines. | |
70 | ``WSGIDaemonProcess`` ``processes`` parameter) will work out of the box. |
|
|||
71 |
|
||||
72 | In order to scale horizontally on multiple machines, you need to do the |
|
67 | In order to scale horizontally on multiple machines, you need to do the | |
73 | following: |
|
68 | following: | |
74 |
|
69 | |||
75 |
|
|
70 | - Each instance's ``data`` storage needs to be configured to be stored on a | |
76 |
|
|
71 | shared disk storage, preferably together with repositories. This ``data`` | |
77 |
|
|
72 | dir contains template caches, sessions, whoosh index and is used for | |
78 |
|
|
73 | task locking (so it is safe across multiple instances). Set the | |
79 |
|
|
74 | ``cache_dir``, ``index_dir``, ``beaker.cache.data_dir``, ``beaker.cache.lock_dir`` | |
80 |
|
|
75 | variables in each .ini file to a shared location across Kallithea instances | |
81 |
|
|
76 | - If using several Celery instances, | |
82 |
|
|
77 | the message broker should be common to all of them (e.g., one | |
83 |
|
|
78 | shared RabbitMQ server) | |
84 |
|
|
79 | - Load balance using round robin or IP hash, recommended is writing LB rules | |
85 |
|
|
80 | that will separate regular user traffic from automated processes like CI | |
86 |
|
|
81 | servers or build bots. | |
87 |
|
82 | |||
88 |
|
83 | |||
89 | Serve static files directly from the web server |
|
84 | Serve static files directly from the web server | |
@@ -125,3 +120,6 b' response. See the documentation for your' | |||||
125 |
|
120 | |||
126 |
|
121 | |||
127 | .. _SQLAlchemyGrate: https://github.com/shazow/sqlalchemygrate |
|
122 | .. _SQLAlchemyGrate: https://github.com/shazow/sqlalchemygrate | |
|
123 | .. _mod_wsgi: https://modwsgi.readthedocs.io/ | |||
|
124 | .. _uWSGI: https://uwsgi-docs.readthedocs.io/ | |||
|
125 | .. _gunicorn: http://pypi.python.org/pypi/gunicorn |
@@ -43,12 +43,19 b' Troubleshooting' | |||||
43 | | |
|
43 | | | |
44 |
|
44 | |||
45 | :Q: **How can I use hooks in Kallithea?** |
|
45 | :Q: **How can I use hooks in Kallithea?** | |
46 | :A: It's easy if they are Python hooks: just use advanced link in |
|
46 | :A: If using Mercurial, use *Admin > Settings > Hooks* to install | |
47 | hooks section in Admin panel, that works only for Mercurial. If |
|
47 | global hooks. Inside the hooks, you can use the current working directory to | |
48 | you want to use Git hooks, just install th proper one in the repository, |
|
48 | control different behaviour for different repositories. | |
49 | e.g., create a file `/gitrepo/hooks/pre-receive`. You can also use |
|
49 | ||
50 | Kallithea-extensions to connect to callback hooks, for both Git |
|
50 | If using Git, install the hooks manually in each repository, for example by | |
51 | and Mercurial. |
|
51 | creating a file ``gitrepo/hooks/pre-receive``. | |
|
52 | Note that Kallithea uses the ``post-receive`` hook internally. | |||
|
53 | Kallithea will not work properly if another post-receive hook is installed instead. | |||
|
54 | You might also accidentally overwrite your own post-receive hook with the Kallithea hook. | |||
|
55 | Instead, put your post-receive hook in ``post-receive-custom``, and the Kallithea hook will invoke it. | |||
|
56 | ||||
|
57 | You can also use Kallithea-extensions to connect to callback hooks, | |||
|
58 | for both Git and Mercurial. | |||
52 |
|
59 | |||
53 | | |
|
60 | | | |
54 |
|
61 |
@@ -37,7 +37,7 b' DAEMON_OPTS="serve --daemon \\' | |||||
37 |
|
37 | |||
38 | start() { |
|
38 | start() { | |
39 | echo "Starting $APP_NAME" |
|
39 | echo "Starting $APP_NAME" | |
40 |
|
|
40 | start-stop-daemon -d $APP_PATH \ | |
41 | --start --quiet \ |
|
41 | --start --quiet \ | |
42 | --pidfile $PID_PATH \ |
|
42 | --pidfile $PID_PATH \ | |
43 | --user $RUN_AS \ |
|
43 | --user $RUN_AS \ |
@@ -33,7 +33,7 b' depend() {' | |||||
33 |
|
33 | |||
34 | start() { |
|
34 | start() { | |
35 | ebegin "Starting $APP_NAME" |
|
35 | ebegin "Starting $APP_NAME" | |
36 |
start-stop-daemon -d $APP_PATH |
|
36 | start-stop-daemon -d $APP_PATH \ | |
37 | --start --quiet \ |
|
37 | --start --quiet \ | |
38 | --pidfile $PID_PATH \ |
|
38 | --pidfile $PID_PATH \ | |
39 | --user $RUN_AS \ |
|
39 | --user $RUN_AS \ |
@@ -63,7 +63,7 b' ensure_pid_dir () {' | |||||
63 |
|
63 | |||
64 | start_kallithea () { |
|
64 | start_kallithea () { | |
65 | ensure_pid_dir |
|
65 | ensure_pid_dir | |
66 |
|
|
66 | daemon --pidfile $PID_PATH \ | |
67 | --user $RUN_AS "$DAEMON $DAEMON_OPTS" |
|
67 | --user $RUN_AS "$DAEMON $DAEMON_OPTS" | |
68 | RETVAL=$? |
|
68 | RETVAL=$? | |
69 | [ $RETVAL -eq 0 ] && touch $LOCK_FILE |
|
69 | [ $RETVAL -eq 0 ] && touch $LOCK_FILE |
@@ -30,20 +30,26 b' Original author and date, and relevant c' | |||||
30 | import platform |
|
30 | import platform | |
31 | import sys |
|
31 | import sys | |
32 |
|
32 | |||
|
33 | import celery | |||
|
34 | ||||
33 |
|
35 | |||
34 | if sys.version_info < (3, 6): |
|
36 | if sys.version_info < (3, 6): | |
35 | raise Exception('Kallithea requires python 3.6 or later') |
|
37 | raise Exception('Kallithea requires python 3.6 or later') | |
36 |
|
38 | |||
37 |
VERSION = (0, 6, |
|
39 | VERSION = (0, 6, 99) | |
38 | BACKENDS = { |
|
40 | BACKENDS = { | |
39 | 'hg': 'Mercurial repository', |
|
41 | 'hg': 'Mercurial repository', | |
40 | 'git': 'Git repository', |
|
42 | 'git': 'Git repository', | |
41 | } |
|
43 | } | |
42 |
|
44 | |||
43 | CELERY_APP = None # set to Celery app instance if using Celery |
|
45 | CELERY_APP = celery.Celery() # needed at import time but is lazy and can be configured later | |
44 | CELERY_EAGER = False |
|
|||
45 |
|
46 | |||
46 | CONFIG = {} |
|
47 | DEFAULT_USER_ID: int # set by setup_configuration | |
|
48 | CONFIG = {} # set to tg.config when TG app is initialized and calls app_cfg | |||
|
49 | ||||
|
50 | # URL prefix for non repository related links - must start with `/` | |||
|
51 | ADMIN_PREFIX = '/_admin' | |||
|
52 | URL_SEP = '/' | |||
47 |
|
53 | |||
48 | # Linked module for extensions |
|
54 | # Linked module for extensions | |
49 | EXTENSIONS = {} |
|
55 | EXTENSIONS = {} |
@@ -21,7 +21,7 b' from logging.config import fileConfig' | |||||
21 | from alembic import context |
|
21 | from alembic import context | |
22 | from sqlalchemy import engine_from_config, pool |
|
22 | from sqlalchemy import engine_from_config, pool | |
23 |
|
23 | |||
24 |
from kallithea.model import |
|
24 | from kallithea.model import meta | |
25 |
|
25 | |||
26 |
|
26 | |||
27 | # The alembic.config.Config object, which wraps the current .ini file. |
|
27 | # The alembic.config.Config object, which wraps the current .ini file. | |
@@ -93,7 +93,7 b' def run_migrations_online():' | |||||
93 |
|
93 | |||
94 | # Support autogeneration of migration scripts based on "diff" between |
|
94 | # Support autogeneration of migration scripts based on "diff" between | |
95 | # current database schema and kallithea.model.db schema. |
|
95 | # current database schema and kallithea.model.db schema. | |
96 |
target_metadata= |
|
96 | target_metadata=meta.Base.metadata, | |
97 | include_object=include_in_autogeneration, |
|
97 | include_object=include_in_autogeneration, | |
98 | render_as_batch=True, # batch mode is needed for SQLite support |
|
98 | render_as_batch=True, # batch mode is needed for SQLite support | |
99 | ) |
|
99 | ) |
@@ -29,7 +29,7 b' depends_on = None' | |||||
29 | from alembic import op |
|
29 | from alembic import op | |
30 | from sqlalchemy import MetaData, Table |
|
30 | from sqlalchemy import MetaData, Table | |
31 |
|
31 | |||
32 |
from kallithea.model |
|
32 | from kallithea.model import db | |
33 |
|
33 | |||
34 |
|
34 | |||
35 | meta = MetaData() |
|
35 | meta = MetaData() | |
@@ -37,7 +37,7 b' meta = MetaData()' | |||||
37 |
|
37 | |||
38 | def upgrade(): |
|
38 | def upgrade(): | |
39 | meta.bind = op.get_bind() |
|
39 | meta.bind = op.get_bind() | |
40 | ui = Table(Ui.__tablename__, meta, autoload=True) |
|
40 | ui = Table(db.Ui.__tablename__, meta, autoload=True) | |
41 |
|
41 | |||
42 | ui.update(values={ |
|
42 | ui.update(values={ | |
43 | 'ui_key': 'prechangegroup.push_lock_handling', |
|
43 | 'ui_key': 'prechangegroup.push_lock_handling', | |
@@ -51,7 +51,7 b' def upgrade():' | |||||
51 |
|
51 | |||
52 | def downgrade(): |
|
52 | def downgrade(): | |
53 | meta.bind = op.get_bind() |
|
53 | meta.bind = op.get_bind() | |
54 | ui = Table(Ui.__tablename__, meta, autoload=True) |
|
54 | ui = Table(db.Ui.__tablename__, meta, autoload=True) | |
55 |
|
55 | |||
56 | ui.update(values={ |
|
56 | ui.update(values={ | |
57 | 'ui_key': 'prechangegroup.pre_push', |
|
57 | 'ui_key': 'prechangegroup.pre_push', |
@@ -30,7 +30,7 b' import sqlalchemy as sa' | |||||
30 | from alembic import op |
|
30 | from alembic import op | |
31 | from sqlalchemy import MetaData, Table |
|
31 | from sqlalchemy import MetaData, Table | |
32 |
|
32 | |||
33 |
from kallithea.model |
|
33 | from kallithea.model import db | |
34 |
|
34 | |||
35 |
|
35 | |||
36 | meta = MetaData() |
|
36 | meta = MetaData() | |
@@ -45,7 +45,7 b' def upgrade():' | |||||
45 | batch_op.drop_column('enable_locking') |
|
45 | batch_op.drop_column('enable_locking') | |
46 |
|
46 | |||
47 | meta.bind = op.get_bind() |
|
47 | meta.bind = op.get_bind() | |
48 | ui = Table(Ui.__tablename__, meta, autoload=True) |
|
48 | ui = Table(db.Ui.__tablename__, meta, autoload=True) | |
49 | ui.delete().where(ui.c.ui_key == 'prechangegroup.push_lock_handling').execute() |
|
49 | ui.delete().where(ui.c.ui_key == 'prechangegroup.push_lock_handling').execute() | |
50 | ui.delete().where(ui.c.ui_key == 'preoutgoing.pull_lock_handling').execute() |
|
50 | ui.delete().where(ui.c.ui_key == 'preoutgoing.pull_lock_handling').execute() | |
51 |
|
51 |
@@ -23,7 +23,7 b' import click' | |||||
23 | import paste.deploy |
|
23 | import paste.deploy | |
24 |
|
24 | |||
25 | import kallithea |
|
25 | import kallithea | |
26 |
import kallithea.config. |
|
26 | import kallithea.config.application | |
27 |
|
27 | |||
28 |
|
28 | |||
29 | # kallithea_cli is usually invoked through the 'kallithea-cli' wrapper script |
|
29 | # kallithea_cli is usually invoked through the 'kallithea-cli' wrapper script | |
@@ -53,10 +53,10 b' def read_config(ini_file_name, strip_sec' | |||||
53 | def cli(): |
|
53 | def cli(): | |
54 | """Various commands to manage a Kallithea instance.""" |
|
54 | """Various commands to manage a Kallithea instance.""" | |
55 |
|
55 | |||
56 | def register_command(config_file=False, config_file_initialize_app=False, hidden=False): |
|
56 | def register_command(needs_config_file=False, config_file_initialize_app=False, hidden=False): | |
57 | """Register a kallithea-cli subcommand. |
|
57 | """Register a kallithea-cli subcommand. | |
58 |
|
58 | |||
59 | If one of the config_file flags are true, a config file must be specified |
|
59 | If one of the needs_config_file flags are true, a config file must be specified | |
60 | with -c and it is read and logging is configured. The configuration is |
|
60 | with -c and it is read and logging is configured. The configuration is | |
61 | available in the kallithea.CONFIG dict. |
|
61 | available in the kallithea.CONFIG dict. | |
62 |
|
62 | |||
@@ -64,21 +64,23 b' def register_command(config_file=False, ' | |||||
64 | (including tg.config), and database access will also be fully initialized. |
|
64 | (including tg.config), and database access will also be fully initialized. | |
65 | """ |
|
65 | """ | |
66 | cli_command = cli.command(hidden=hidden) |
|
66 | cli_command = cli.command(hidden=hidden) | |
67 | if config_file or config_file_initialize_app: |
|
67 | if needs_config_file or config_file_initialize_app: | |
68 | def annotator(annotated): |
|
68 | def annotator(annotated): | |
69 | @click.option('--config_file', '-c', help="Path to .ini file with app configuration.", |
|
69 | @click.option('--config_file', '-c', help="Path to .ini file with app configuration.", | |
70 | type=click.Path(dir_okay=False, exists=True, readable=True), required=True) |
|
70 | type=click.Path(dir_okay=False, exists=True, readable=True), required=True) | |
71 | @functools.wraps(annotated) # reuse meta data from the wrapped function so click can see other options |
|
71 | @functools.wraps(annotated) # reuse meta data from the wrapped function so click can see other options | |
72 | def runtime_wrapper(config_file, *args, **kwargs): |
|
72 | def runtime_wrapper(config_file, *args, **kwargs): | |
73 | path_to_ini_file = os.path.realpath(config_file) |
|
73 | path_to_ini_file = os.path.realpath(config_file) | |
74 |
|
|
74 | config = paste.deploy.appconfig('config:' + path_to_ini_file) | |
75 | cp = configparser.ConfigParser(strict=False) |
|
75 | cp = configparser.ConfigParser(strict=False) | |
76 | cp.read_string(read_config(path_to_ini_file, strip_section_prefix=annotated.__name__)) |
|
76 | cp.read_string(read_config(path_to_ini_file, strip_section_prefix=annotated.__name__)) | |
77 | logging.config.fileConfig(cp, |
|
77 | logging.config.fileConfig(cp, | |
78 | {'__file__': path_to_ini_file, 'here': os.path.dirname(path_to_ini_file)}) |
|
78 | {'__file__': path_to_ini_file, 'here': os.path.dirname(path_to_ini_file)}) | |
|
79 | if needs_config_file: | |||
|
80 | annotated(*args, config=config, **kwargs) | |||
79 | if config_file_initialize_app: |
|
81 | if config_file_initialize_app: | |
80 |
kallithea.config. |
|
82 | kallithea.config.application.make_app(config.global_conf, **config.local_conf) | |
81 |
|
|
83 | annotated(*args, **kwargs) | |
82 | return cli_command(runtime_wrapper) |
|
84 | return cli_command(runtime_wrapper) | |
83 | return annotator |
|
85 | return annotator | |
84 | return cli_command |
|
86 | return cli_command |
@@ -12,16 +12,18 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 |
|
14 | |||
15 | import celery.bin.worker |
|
|||
16 | import click |
|
15 | import click | |
|
16 | from celery.bin.celery import celery as celery_command | |||
17 |
|
17 | |||
18 | import kallithea |
|
18 | import kallithea | |
19 | import kallithea.bin.kallithea_cli_base as cli_base |
|
19 | import kallithea.bin.kallithea_cli_base as cli_base | |
|
20 | from kallithea.lib import celery_app | |||
|
21 | from kallithea.lib.utils2 import asbool | |||
20 |
|
22 | |||
21 |
|
23 | |||
22 |
@cli_base.register_command(config_file |
|
24 | @cli_base.register_command(needs_config_file=True) | |
23 | @click.argument('celery_args', nargs=-1) |
|
25 | @click.argument('celery_args', nargs=-1) | |
24 | def celery_run(celery_args): |
|
26 | def celery_run(celery_args, config): | |
25 | """Start Celery worker(s) for asynchronous tasks. |
|
27 | """Start Celery worker(s) for asynchronous tasks. | |
26 |
|
28 | |||
27 | This commands starts the Celery daemon which will spawn workers to handle |
|
29 | This commands starts the Celery daemon which will spawn workers to handle | |
@@ -32,9 +34,20 b' def celery_run(celery_args):' | |||||
32 | by this CLI command. |
|
34 | by this CLI command. | |
33 | """ |
|
35 | """ | |
34 |
|
36 | |||
35 | if not kallithea.CELERY_APP: |
|
37 | if not asbool(config.get('use_celery')): | |
36 | raise Exception('Please set use_celery = true in .ini config ' |
|
38 | raise Exception('Please set use_celery = true in .ini config ' | |
37 | 'file before running this command') |
|
39 | 'file before running this command') | |
38 |
|
40 | |||
39 | cmd = celery.bin.worker.worker(kallithea.CELERY_APP) |
|
41 | kallithea.CELERY_APP.config_from_object(celery_app.make_celery_config(config)) | |
40 | return cmd.run_from_argv(None, command='celery-run -c CONFIG_FILE --', argv=list(celery_args)) |
|
42 | ||
|
43 | kallithea.CELERY_APP.loader.on_worker_process_init = lambda: kallithea.config.application.make_app(config.global_conf, **config.local_conf) | |||
|
44 | ||||
|
45 | args = list(celery_args) | |||
|
46 | # args[0] is generally ignored when prog_name is specified, but -h *needs* it to be 'worker' ... but will also suggest that users specify 'worker' explicitly | |||
|
47 | if not args or args[0] != 'worker': | |||
|
48 | args.insert(0, 'worker') | |||
|
49 | ||||
|
50 | # inline kallithea.CELERY_APP.start in order to allow specifying prog_name | |||
|
51 | assert celery_command.params[0].name == 'app' | |||
|
52 | celery_command.params[0].default = kallithea.CELERY_APP | |||
|
53 | celery_command.main(args=args, prog_name='kallithea-cli celery-run -c CONFIG_FILE --') |
@@ -21,7 +21,7 b' import click' | |||||
21 | import mako.exceptions |
|
21 | import mako.exceptions | |
22 |
|
22 | |||
23 | import kallithea.bin.kallithea_cli_base as cli_base |
|
23 | import kallithea.bin.kallithea_cli_base as cli_base | |
24 | import kallithea.lib.locale |
|
24 | import kallithea.lib.locales | |
25 | from kallithea.lib import inifile |
|
25 | from kallithea.lib import inifile | |
26 |
|
26 | |||
27 |
|
27 | |||
@@ -66,7 +66,7 b' def config_create(config_file, key_value' | |||||
66 | 'git_hook_interpreter': sys.executable, |
|
66 | 'git_hook_interpreter': sys.executable, | |
67 | 'user_home_path': os.path.expanduser('~'), |
|
67 | 'user_home_path': os.path.expanduser('~'), | |
68 | 'kallithea_cli_path': cli_base.kallithea_cli_path, |
|
68 | 'kallithea_cli_path': cli_base.kallithea_cli_path, | |
69 | 'ssh_locale': kallithea.lib.locale.get_current_locale(), |
|
69 | 'ssh_locale': kallithea.lib.locales.get_current_locale(), | |
70 | } |
|
70 | } | |
71 | ini_settings = defaultdict(dict) |
|
71 | ini_settings = defaultdict(dict) | |
72 |
|
72 |
@@ -15,11 +15,15 b' import click' | |||||
15 |
|
15 | |||
16 | import kallithea |
|
16 | import kallithea | |
17 | import kallithea.bin.kallithea_cli_base as cli_base |
|
17 | import kallithea.bin.kallithea_cli_base as cli_base | |
|
18 | import kallithea.lib.utils | |||
|
19 | import kallithea.model.scm | |||
18 | from kallithea.lib.db_manage import DbManage |
|
20 | from kallithea.lib.db_manage import DbManage | |
19 |
from kallithea.model |
|
21 | from kallithea.model import meta | |
20 |
|
22 | |||
21 |
|
23 | |||
22 | @cli_base.register_command(config_file=True) |
|
24 | @cli_base.register_command(needs_config_file=True, config_file_initialize_app=True) | |
|
25 | @click.option('--reuse/--no-reuse', default=False, | |||
|
26 | help='Reuse and clean existing database instead of dropping and creating (default: no reuse)') | |||
23 | @click.option('--user', help='Username of administrator account.') |
|
27 | @click.option('--user', help='Username of administrator account.') | |
24 | @click.option('--password', help='Password for administrator account.') |
|
28 | @click.option('--password', help='Password for administrator account.') | |
25 | @click.option('--email', help='Email address of administrator account.') |
|
29 | @click.option('--email', help='Email address of administrator account.') | |
@@ -28,7 +32,7 b' from kallithea.model.meta import Session' | |||||
28 | @click.option('--force-no', is_flag=True, help='Answer no to every question.') |
|
32 | @click.option('--force-no', is_flag=True, help='Answer no to every question.') | |
29 | @click.option('--public-access/--no-public-access', default=True, |
|
33 | @click.option('--public-access/--no-public-access', default=True, | |
30 | help='Enable/disable public access on this installation (default: enable)') |
|
34 | help='Enable/disable public access on this installation (default: enable)') | |
31 | def db_create(user, password, email, repos, force_yes, force_no, public_access): |
|
35 | def db_create(user, password, email, repos, force_yes, force_no, public_access, reuse, config=None): | |
32 | """Initialize the database. |
|
36 | """Initialize the database. | |
33 |
|
37 | |||
34 | Create all required tables in the database specified in the configuration |
|
38 | Create all required tables in the database specified in the configuration | |
@@ -37,44 +41,43 b' def db_create(user, password, email, rep' | |||||
37 |
|
41 | |||
38 | You can pass the answers to all questions as options to this command. |
|
42 | You can pass the answers to all questions as options to this command. | |
39 | """ |
|
43 | """ | |
40 | dbconf = kallithea.CONFIG['sqlalchemy.url'] |
|
44 | if config is not None: # first called with config, before app initialization | |
|
45 | dbconf = config['sqlalchemy.url'] | |||
41 |
|
46 | |||
42 | # force_ask should be True (yes), False (no), or None (ask) |
|
47 | # force_ask should be True (yes), False (no), or None (ask) | |
43 | if force_yes: |
|
48 | if force_yes: | |
44 | force_ask = True |
|
49 | force_ask = True | |
45 | elif force_no: |
|
50 | elif force_no: | |
46 | force_ask = False |
|
51 | force_ask = False | |
47 | else: |
|
52 | else: | |
48 | force_ask = None |
|
53 | force_ask = None | |
49 |
|
54 | |||
50 | cli_args = dict( |
|
55 | cli_args = dict( | |
51 | username=user, |
|
56 | username=user, | |
52 | password=password, |
|
57 | password=password, | |
53 | email=email, |
|
58 | email=email, | |
54 | repos_location=repos, |
|
59 | repos_location=repos, | |
55 | force_ask=force_ask, |
|
60 | force_ask=force_ask, | |
56 | public_access=public_access, |
|
61 | public_access=public_access, | |
57 | ) |
|
62 | ) | |
58 |
dbmanage = DbManage(dbconf=dbconf, root= |
|
63 | dbmanage = DbManage(dbconf=dbconf, root=config['here'], | |
59 |
|
|
64 | cli_args=cli_args) | |
60 |
dbmanage.create_tables( |
|
65 | dbmanage.create_tables(reuse_database=reuse) | |
61 | repo_root_path = dbmanage.prompt_repo_root_path(None) |
|
66 | repo_root_path = dbmanage.prompt_repo_root_path(None) | |
62 | dbmanage.create_settings(repo_root_path) |
|
67 | dbmanage.create_settings(repo_root_path) | |
63 | dbmanage.create_default_user() |
|
68 | dbmanage.create_default_user() | |
64 |
dbmanage.admin_ |
|
69 | dbmanage.create_admin_user() | |
65 | dbmanage.create_permissions() |
|
70 | dbmanage.create_permissions() | |
66 | dbmanage.populate_default_permissions() |
|
71 | dbmanage.populate_default_permissions() | |
67 | Session().commit() |
|
72 | meta.Session().commit() | |
68 |
|
73 | |||
69 | # initial repository scan |
|
74 | else: # then called again after app initialization | |
70 | kallithea.config.middleware.make_app( |
|
75 | added, _ = kallithea.lib.utils.repo2db_mapper(kallithea.model.scm.ScmModel().repo_scan()) | |
71 | kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf) |
|
76 | if added: | |
72 | added, _ = kallithea.lib.utils.repo2db_mapper(kallithea.model.scm.ScmModel().repo_scan()) |
|
77 | click.echo('Initial repository scan: added following repositories:') | |
73 | if added: |
|
78 | click.echo('\t%s' % '\n\t'.join(added)) | |
74 | click.echo('Initial repository scan: added following repositories:') |
|
79 | else: | |
75 | click.echo('\t%s' % '\n\t'.join(added)) |
|
80 | click.echo('Initial repository scan: no repositories found.') | |
76 | else: |
|
|||
77 | click.echo('Initial repository scan: no repositories found.') |
|
|||
78 |
|
81 | |||
79 | click.echo('Database set up successfully.') |
|
82 | click.echo('Database set up successfully.') | |
80 | click.echo("Don't forget to build the front-end using 'kallithea-cli front-end-build'.") |
|
83 | click.echo("Don't forget to build the front-end using 'kallithea-cli front-end-build'.") |
@@ -24,24 +24,23 b' import os' | |||||
24 | import click |
|
24 | import click | |
25 | import pkg_resources |
|
25 | import pkg_resources | |
26 |
|
26 | |||
27 | import kallithea |
|
|||
28 | import kallithea.bin.kallithea_cli_base as cli_base |
|
27 | import kallithea.bin.kallithea_cli_base as cli_base | |
29 | from kallithea.lib.utils2 import ask_ok |
|
28 | from kallithea.lib.utils2 import ask_ok | |
30 |
|
29 | |||
31 |
|
30 | |||
32 | @cli_base.register_command(config_file=True) |
|
31 | @cli_base.register_command(needs_config_file=True) | |
33 | def extensions_create(): |
|
32 | def extensions_create(config): | |
34 | """Write template file for extending Kallithea in Python. |
|
33 | """Write template file for extending Kallithea in Python. | |
35 |
|
34 | |||
36 | An rcextensions directory with a __init__.py file will be created next to |
|
35 | Create a template `extensions.py` file next to the ini file. Local | |
37 |
|
|
36 | customizations in that file will survive upgrades. The file contains | |
38 |
|
|
37 | instructions on how it can be customized. | |
39 | """ |
|
38 | """ | |
40 |
here = |
|
39 | here = config['here'] | |
41 | content = pkg_resources.resource_string( |
|
40 | content = pkg_resources.resource_string( | |
42 |
'kallithea', os.path.join(' |
|
41 | 'kallithea', os.path.join('templates', 'py', 'extensions.py') | |
43 | ) |
|
42 | ) | |
44 |
ext_file = os.path.join(here, ' |
|
43 | ext_file = os.path.join(here, 'extensions.py') | |
45 | if os.path.exists(ext_file): |
|
44 | if os.path.exists(ext_file): | |
46 | msg = ('Extension file %s already exists, do you want ' |
|
45 | msg = ('Extension file %s already exists, do you want ' | |
47 | 'to overwrite it ? [y/n] ') % ext_file |
|
46 | 'to overwrite it ? [y/n] ') % ext_file |
@@ -16,7 +16,6 b' import sys' | |||||
16 |
|
16 | |||
17 | import click |
|
17 | import click | |
18 |
|
18 | |||
19 | import kallithea |
|
|||
20 | import kallithea.bin.kallithea_cli_base as cli_base |
|
19 | import kallithea.bin.kallithea_cli_base as cli_base | |
21 |
|
20 | |||
22 |
|
21 | |||
@@ -57,16 +56,16 b" if __name__=='__main__':" | |||||
57 | HandleCommandLine(params) |
|
56 | HandleCommandLine(params) | |
58 | ''' |
|
57 | ''' | |
59 |
|
58 | |||
60 | @cli_base.register_command(config_file=True) |
|
59 | @cli_base.register_command(needs_config_file=True) | |
61 | @click.option('--virtualdir', default='/', |
|
60 | @click.option('--virtualdir', default='/', | |
62 | help='The virtual folder to install into on IIS.') |
|
61 | help='The virtual folder to install into on IIS.') | |
63 | def iis_install(virtualdir): |
|
62 | def iis_install(virtualdir, config): | |
64 | """Install into IIS using isapi-wsgi.""" |
|
63 | """Install into IIS using isapi-wsgi.""" | |
65 |
|
64 | |||
66 |
config_file_abs = |
|
65 | config_file_abs = config['__file__'] | |
67 |
|
66 | |||
68 | try: |
|
67 | try: | |
69 | import isapi_wsgi |
|
68 | import isapi_wsgi # pytype: disable=import-error | |
70 | assert isapi_wsgi |
|
69 | assert isapi_wsgi | |
71 | except ImportError: |
|
70 | except ImportError: | |
72 | sys.stderr.write('missing requirement: isapi-wsgi not installed\n') |
|
71 | sys.stderr.write('missing requirement: isapi-wsgi not installed\n') |
@@ -28,7 +28,7 b' import kallithea' | |||||
28 | import kallithea.bin.kallithea_cli_base as cli_base |
|
28 | import kallithea.bin.kallithea_cli_base as cli_base | |
29 | from kallithea.lib.indexers.daemon import WhooshIndexingDaemon |
|
29 | from kallithea.lib.indexers.daemon import WhooshIndexingDaemon | |
30 | from kallithea.lib.pidlock import DaemonLock, LockHeld |
|
30 | from kallithea.lib.pidlock import DaemonLock, LockHeld | |
31 |
from kallithea.lib.utils import load_ |
|
31 | from kallithea.lib.utils import load_extensions | |
32 | from kallithea.model.repo import RepoModel |
|
32 | from kallithea.model.repo import RepoModel | |
33 |
|
33 | |||
34 |
|
34 | |||
@@ -41,7 +41,7 b' def index_create(repo_location, index_on' | |||||
41 | """Create or update full text search index""" |
|
41 | """Create or update full text search index""" | |
42 |
|
42 | |||
43 | index_location = kallithea.CONFIG['index_dir'] |
|
43 | index_location = kallithea.CONFIG['index_dir'] | |
44 |
load_ |
|
44 | load_extensions(kallithea.CONFIG['here']) | |
45 |
|
45 | |||
46 | if not repo_location: |
|
46 | if not repo_location: | |
47 | repo_location = RepoModel().repos_path |
|
47 | repo_location = RepoModel().repos_path |
@@ -30,15 +30,18 b' import kallithea' | |||||
30 | import kallithea.bin.kallithea_cli_base as cli_base |
|
30 | import kallithea.bin.kallithea_cli_base as cli_base | |
31 | from kallithea.lib.utils import REMOVED_REPO_PAT, repo2db_mapper |
|
31 | from kallithea.lib.utils import REMOVED_REPO_PAT, repo2db_mapper | |
32 | from kallithea.lib.utils2 import ask_ok |
|
32 | from kallithea.lib.utils2 import ask_ok | |
33 |
from kallithea.model |
|
33 | from kallithea.model import db, meta | |
34 | from kallithea.model.meta import Session |
|
|||
35 | from kallithea.model.scm import ScmModel |
|
34 | from kallithea.model.scm import ScmModel | |
36 |
|
35 | |||
37 |
|
36 | |||
38 | @cli_base.register_command(config_file_initialize_app=True) |
|
37 | @cli_base.register_command(config_file_initialize_app=True) | |
39 | @click.option('--remove-missing', is_flag=True, |
|
38 | @click.option('--remove-missing', is_flag=True, | |
40 | help='Remove missing repositories from the Kallithea database.') |
|
39 | help='Remove missing repositories from the Kallithea database.') | |
41 | def repo_scan(remove_missing): |
|
40 | @click.option('--install-git-hooks', is_flag=True, | |
|
41 | help='(Re)install Kallithea Git hooks without overwriting other hooks.') | |||
|
42 | @click.option('--overwrite-git-hooks', is_flag=True, | |||
|
43 | help='(Re)install Kallithea Git hooks, overwriting other hooks.') | |||
|
44 | def repo_scan(remove_missing, install_git_hooks, overwrite_git_hooks): | |||
42 | """Scan filesystem for repositories. |
|
45 | """Scan filesystem for repositories. | |
43 |
|
46 | |||
44 | Search the configured repository root for new repositories and add them |
|
47 | Search the configured repository root for new repositories and add them | |
@@ -49,7 +52,9 b' def repo_scan(remove_missing):' | |||||
49 | """ |
|
52 | """ | |
50 | click.echo('Now scanning root location for new repos ...') |
|
53 | click.echo('Now scanning root location for new repos ...') | |
51 | added, removed = repo2db_mapper(ScmModel().repo_scan(), |
|
54 | added, removed = repo2db_mapper(ScmModel().repo_scan(), | |
52 |
remove_obsolete=remove_missing |
|
55 | remove_obsolete=remove_missing, | |
|
56 | install_git_hooks=install_git_hooks, | |||
|
57 | overwrite_git_hooks=overwrite_git_hooks) | |||
53 | click.echo('Scan completed.') |
|
58 | click.echo('Scan completed.') | |
54 | if added: |
|
59 | if added: | |
55 | click.echo('Added: %s' % ', '.join(added)) |
|
60 | click.echo('Added: %s' % ', '.join(added)) | |
@@ -73,11 +78,11 b' def repo_update_metadata(repositories):' | |||||
73 | updated. |
|
78 | updated. | |
74 | """ |
|
79 | """ | |
75 | if not repositories: |
|
80 | if not repositories: | |
76 | repo_list = Repository.query().all() |
|
81 | repo_list = db.Repository.query().all() | |
77 | else: |
|
82 | else: | |
78 | repo_names = [n.strip() for n in repositories] |
|
83 | repo_names = [n.strip() for n in repositories] | |
79 | repo_list = list(Repository.query() |
|
84 | repo_list = list(db.Repository.query() | |
80 | .filter(Repository.repo_name.in_(repo_names))) |
|
85 | .filter(db.Repository.repo_name.in_(repo_names))) | |
81 |
|
86 | |||
82 | for repo in repo_list: |
|
87 | for repo in repo_list: | |
83 | # update latest revision metadata in database |
|
88 | # update latest revision metadata in database | |
@@ -86,7 +91,7 b' def repo_update_metadata(repositories):' | |||||
86 | # first access |
|
91 | # first access | |
87 | repo.set_invalidate() |
|
92 | repo.set_invalidate() | |
88 |
|
93 | |||
89 | Session().commit() |
|
94 | meta.Session().commit() | |
90 |
|
95 | |||
91 | click.echo('Updated database with information about latest change in the following %s repositories:' % (len(repo_list))) |
|
96 | click.echo('Updated database with information about latest change in the following %s repositories:' % (len(repo_list))) | |
92 | click.echo('\n'.join(repo.repo_name for repo in repo_list)) |
|
97 | click.echo('\n'.join(repo.repo_name for repo in repo_list)) |
@@ -21,9 +21,9 b' import click' | |||||
21 |
|
21 | |||
22 | import kallithea |
|
22 | import kallithea | |
23 | import kallithea.bin.kallithea_cli_base as cli_base |
|
23 | import kallithea.bin.kallithea_cli_base as cli_base | |
24 |
from kallithea.lib.utils2 import s |
|
24 | from kallithea.lib.utils2 import asbool | |
25 |
from kallithea.lib.vcs. |
|
25 | from kallithea.lib.vcs.ssh.git import GitSshHandler | |
26 |
from kallithea.lib.vcs. |
|
26 | from kallithea.lib.vcs.ssh.hg import MercurialSshHandler | |
27 | from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException |
|
27 | from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException | |
28 |
|
28 | |||
29 |
|
29 | |||
@@ -40,8 +40,7 b' def ssh_serve(user_id, key_id):' | |||||
40 | protocol access. The access will be granted as the specified user ID, and |
|
40 | protocol access. The access will be granted as the specified user ID, and | |
41 | logged as using the specified key ID. |
|
41 | logged as using the specified key ID. | |
42 | """ |
|
42 | """ | |
43 |
|
|
43 | if not asbool(kallithea.CONFIG.get('ssh_enabled', False)): | |
44 | if not str2bool(ssh_enabled): |
|
|||
45 | sys.stderr.write("SSH access is disabled.\n") |
|
44 | sys.stderr.write("SSH access is disabled.\n") | |
46 | return sys.exit(1) |
|
45 | return sys.exit(1) | |
47 |
|
46 | |||
@@ -70,7 +69,7 b' def ssh_serve(user_id, key_id):' | |||||
70 | vcs_handler = VcsHandler.make(ssh_command_parts) |
|
69 | vcs_handler = VcsHandler.make(ssh_command_parts) | |
71 | if vcs_handler is not None: |
|
70 | if vcs_handler is not None: | |
72 | vcs_handler.serve(user_id, key_id, client_ip) |
|
71 | vcs_handler.serve(user_id, key_id, client_ip) | |
73 | assert False # serve is written so it never will terminate |
|
72 | sys.exit(0) | |
74 |
|
73 | |||
75 | sys.stderr.write("This account can only be used for repository access. SSH command %r is not supported.\n" % ssh_original_command) |
|
74 | sys.stderr.write("This account can only be used for repository access. SSH command %r is not supported.\n" % ssh_original_command) | |
76 | sys.exit(1) |
|
75 | sys.exit(1) |
@@ -12,10 +12,10 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 | """ |
|
14 | """ | |
15 |
kallithea. |
|
15 | kallithea.bin.vcs_hooks | |
16 | ~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | Hooks run by Kallithea |
|
18 | Entry points for Kallithea hooking into Mercurial and Git. | |
19 |
|
19 | |||
20 | This file was forked by the Kallithea project in July 2014. |
|
20 | This file was forked by the Kallithea project in July 2014. | |
21 | Original author and date, and relevant copyright and licensing information is below: |
|
21 | Original author and date, and relevant copyright and licensing information is below: | |
@@ -25,279 +25,84 b' Original author and date, and relevant c' | |||||
25 | :license: GPLv3, see LICENSE.md for more details. |
|
25 | :license: GPLv3, see LICENSE.md for more details. | |
26 | """ |
|
26 | """ | |
27 |
|
27 | |||
|
28 | import logging | |||
28 | import os |
|
29 | import os | |
29 | import sys |
|
30 | import sys | |
30 | import time |
|
|||
31 |
|
||||
32 | import mercurial.scmutil |
|
|||
33 |
|
31 | |||
34 | from kallithea.lib import helpers as h |
|
32 | import mercurial.hg | |
35 | from kallithea.lib.exceptions import UserCreationError |
|
33 | import mercurial.scmutil | |
36 | from kallithea.lib.utils import action_logger, make_ui |
|
34 | import paste.deploy | |
|
35 | ||||
|
36 | import kallithea | |||
|
37 | import kallithea.config.application | |||
|
38 | from kallithea.lib import hooks, webutils | |||
37 | from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str |
|
39 | from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str | |
38 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
40 | from kallithea.lib.vcs.backends.base import EmptyChangeset | |
39 | from kallithea.model.db import Repository, User |
|
41 | from kallithea.lib.vcs.utils.helpers import get_scm_size | |
|
42 | from kallithea.model import db | |||
40 |
|
43 | |||
41 |
|
44 | |||
42 | def _get_scm_size(alias, root_path): |
|
45 | log = logging.getLogger(__name__) | |
43 | if not alias.startswith('.'): |
|
|||
44 | alias += '.' |
|
|||
45 |
|
||||
46 | size_scm, size_root = 0, 0 |
|
|||
47 | for path, dirs, files in os.walk(root_path): |
|
|||
48 | if path.find(alias) != -1: |
|
|||
49 | for f in files: |
|
|||
50 | try: |
|
|||
51 | size_scm += os.path.getsize(os.path.join(path, f)) |
|
|||
52 | except OSError: |
|
|||
53 | pass |
|
|||
54 | else: |
|
|||
55 | for f in files: |
|
|||
56 | try: |
|
|||
57 | size_root += os.path.getsize(os.path.join(path, f)) |
|
|||
58 | except OSError: |
|
|||
59 | pass |
|
|||
60 |
|
||||
61 | size_scm_f = h.format_byte_size(size_scm) |
|
|||
62 | size_root_f = h.format_byte_size(size_root) |
|
|||
63 | size_total_f = h.format_byte_size(size_root + size_scm) |
|
|||
64 |
|
||||
65 | return size_scm_f, size_root_f, size_total_f |
|
|||
66 |
|
46 | |||
67 |
|
47 | |||
68 | def repo_size(ui, repo, hooktype=None, **kwargs): |
|
48 | def repo_size(ui, repo, hooktype=None, **kwargs): | |
69 | """Show size of Mercurial repository. |
|
49 | """Show size of Mercurial repository. | |
70 |
|
50 | |||
71 | Called as Mercurial hook changegroup.repo_size after push. |
|
51 | Called as Mercurial hook changegroup.kallithea_repo_size after push. | |
72 | """ |
|
52 | """ | |
73 |
size_hg |
|
53 | size_hg, size_root = get_scm_size('.hg', safe_str(repo.root)) | |
74 |
|
54 | |||
75 | last_cs = repo[len(repo) - 1] |
|
55 | last_cs = repo[len(repo) - 1] | |
76 |
|
56 | |||
77 | msg = ('Repository size .hg: %s Checkout: %s Total: %s\n' |
|
57 | msg = ('Repository size .hg: %s Checkout: %s Total: %s\n' | |
78 | 'Last revision is now r%s:%s\n') % ( |
|
58 | 'Last revision is now r%s:%s\n') % ( | |
79 | size_hg_f, size_root_f, size_total_f, last_cs.rev(), ascii_str(last_cs.hex())[:12] |
|
59 | webutils.format_byte_size(size_hg), | |
|
60 | webutils.format_byte_size(size_root), | |||
|
61 | webutils.format_byte_size(size_hg + size_root), | |||
|
62 | last_cs.rev(), | |||
|
63 | ascii_str(last_cs.hex())[:12], | |||
80 | ) |
|
64 | ) | |
81 | ui.status(safe_bytes(msg)) |
|
65 | ui.status(safe_bytes(msg)) | |
82 |
|
66 | |||
83 |
|
67 | |||
84 |
def |
|
68 | def update(ui, repo, hooktype=None, **kwargs): | |
85 | """Logs user last pull action |
|
69 | """Update repo after push. The equivalent to 'hg update' but using the same | |
86 |
|
70 | Mercurial as everything else. | ||
87 | Called as Mercurial hook outgoing.pull_logger or from Kallithea before invoking Git. |
|
|||
88 |
|
||||
89 | Does *not* use the action from the hook environment but is always 'pull'. |
|
|||
90 | """ |
|
|||
91 | ex = get_hook_environment() |
|
|||
92 |
|
|
71 | ||
93 | user = User.get_by_username(ex.username) |
|
72 | Called as Mercurial hook changegroup.kallithea_update after push. | |
94 | action = 'pull' |
|
73 | """ | |
95 | action_logger(user, action, ex.repository, ex.ip, commit=True) |
|
74 | try: | |
96 | # extension hook call |
|
75 | ui.pushbuffer(error=True, subproc=True) | |
97 | from kallithea import EXTENSIONS |
|
76 | rev = brev = None | |
98 | callback = getattr(EXTENSIONS, 'PULL_HOOK', None) |
|
77 | mercurial.hg.updatetotally(ui, repo, rev, brev) | |
99 | if callable(callback): |
|
78 | finally: | |
100 | kw = {} |
|
79 | s = ui.popbuffer() # usually just "x files updated, x files merged, x files removed, x files unresolved" | |
101 | kw.update(ex) |
|
80 | log.info('%s update hook output: %s', safe_str(repo.root), safe_str(s).rstrip()) | |
102 | callback(**kw) |
|
|||
103 |
|
||||
104 | return 0 |
|
|||
105 |
|
81 | |||
106 |
|
82 | |||
107 |
def |
|
83 | def pull_action(ui, repo, **kwargs): | |
|
84 | """Logs user pull action | |||
|
85 | ||||
|
86 | Called as Mercurial hook outgoing.kallithea_pull_action. | |||
|
87 | """ | |||
|
88 | hooks.log_pull_action() | |||
|
89 | ||||
|
90 | ||||
|
91 | def push_action(ui, repo, node, node_last, **kwargs): | |||
108 | """ |
|
92 | """ | |
109 | Register that changes have been added to the repo - log the action *and* invalidate caches. |
|
93 | Register that changes have been added to the repo - log the action *and* invalidate caches. | |
110 | Note: This hook is not only logging, but also the side effect invalidating |
|
94 | Note: This hook is not only logging, but also the side effect invalidating | |
111 | caches! The function should perhaps be renamed. |
|
95 | caches! The function should perhaps be renamed. | |
112 |
|
96 | |||
113 |
Called as Mercurial hook changegroup.kallithea_ |
|
97 | Called as Mercurial hook changegroup.kallithea_push_action . | |
114 |
|
98 | |||
115 | The pushed changesets is given by the revset 'node:node_last'. |
|
99 | The pushed changesets is given by the revset 'node:node_last'. | |
116 | """ |
|
100 | """ | |
117 | revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])] |
|
101 | revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])] | |
118 | process_pushed_raw_ids(revs) |
|
102 | hooks.process_pushed_raw_ids(revs) | |
119 | return 0 |
|
|||
120 |
|
||||
121 |
|
||||
122 | def process_pushed_raw_ids(revs): |
|
|||
123 | """ |
|
|||
124 | Register that changes have been added to the repo - log the action *and* invalidate caches. |
|
|||
125 |
|
||||
126 | Called from Mercurial changegroup.kallithea_log_push_action calling hook log_push_action, |
|
|||
127 | or from the Git post-receive hook calling handle_git_post_receive ... |
|
|||
128 | or from scm _handle_push. |
|
|||
129 | """ |
|
|||
130 | ex = get_hook_environment() |
|
|||
131 |
|
||||
132 | action = '%s:%s' % (ex.action, ','.join(revs)) |
|
|||
133 | action_logger(ex.username, action, ex.repository, ex.ip, commit=True) |
|
|||
134 |
|
||||
135 | from kallithea.model.scm import ScmModel |
|
|||
136 | ScmModel().mark_for_invalidation(ex.repository) |
|
|||
137 |
|
||||
138 | # extension hook call |
|
|||
139 | from kallithea import EXTENSIONS |
|
|||
140 | callback = getattr(EXTENSIONS, 'PUSH_HOOK', None) |
|
|||
141 | if callable(callback): |
|
|||
142 | kw = {'pushed_revs': revs} |
|
|||
143 | kw.update(ex) |
|
|||
144 | callback(**kw) |
|
|||
145 |
|
||||
146 |
|
||||
147 | def log_create_repository(repository_dict, created_by, **kwargs): |
|
|||
148 | """ |
|
|||
149 | Post create repository Hook. |
|
|||
150 |
|
||||
151 | :param repository: dict dump of repository object |
|
|||
152 | :param created_by: username who created repository |
|
|||
153 |
|
||||
154 | available keys of repository_dict: |
|
|||
155 |
|
||||
156 | 'repo_type', |
|
|||
157 | 'description', |
|
|||
158 | 'private', |
|
|||
159 | 'created_on', |
|
|||
160 | 'enable_downloads', |
|
|||
161 | 'repo_id', |
|
|||
162 | 'owner_id', |
|
|||
163 | 'enable_statistics', |
|
|||
164 | 'clone_uri', |
|
|||
165 | 'fork_id', |
|
|||
166 | 'group_id', |
|
|||
167 | 'repo_name' |
|
|||
168 |
|
||||
169 | """ |
|
|||
170 | from kallithea import EXTENSIONS |
|
|||
171 | callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None) |
|
|||
172 | if callable(callback): |
|
|||
173 | kw = {} |
|
|||
174 | kw.update(repository_dict) |
|
|||
175 | kw.update({'created_by': created_by}) |
|
|||
176 | kw.update(kwargs) |
|
|||
177 | return callback(**kw) |
|
|||
178 |
|
||||
179 | return 0 |
|
|||
180 |
|
||||
181 |
|
||||
182 | def check_allowed_create_user(user_dict, created_by, **kwargs): |
|
|||
183 | # pre create hooks |
|
|||
184 | from kallithea import EXTENSIONS |
|
|||
185 | callback = getattr(EXTENSIONS, 'PRE_CREATE_USER_HOOK', None) |
|
|||
186 | if callable(callback): |
|
|||
187 | allowed, reason = callback(created_by=created_by, **user_dict) |
|
|||
188 | if not allowed: |
|
|||
189 | raise UserCreationError(reason) |
|
|||
190 |
|
103 | |||
191 |
|
104 | |||
192 | def log_create_user(user_dict, created_by, **kwargs): |
|
105 | def _git_hook_environment(repo_path): | |
193 | """ |
|
|||
194 | Post create user Hook. |
|
|||
195 |
|
||||
196 | :param user_dict: dict dump of user object |
|
|||
197 |
|
||||
198 | available keys for user_dict: |
|
|||
199 |
|
||||
200 | 'username', |
|
|||
201 | 'full_name_or_username', |
|
|||
202 | 'full_contact', |
|
|||
203 | 'user_id', |
|
|||
204 | 'name', |
|
|||
205 | 'firstname', |
|
|||
206 | 'short_contact', |
|
|||
207 | 'admin', |
|
|||
208 | 'lastname', |
|
|||
209 | 'ip_addresses', |
|
|||
210 | 'ldap_dn', |
|
|||
211 | 'email', |
|
|||
212 | 'api_key', |
|
|||
213 | 'last_login', |
|
|||
214 | 'full_name', |
|
|||
215 | 'active', |
|
|||
216 | 'password', |
|
|||
217 | 'emails', |
|
|||
218 |
|
||||
219 | """ |
|
|||
220 | from kallithea import EXTENSIONS |
|
|||
221 | callback = getattr(EXTENSIONS, 'CREATE_USER_HOOK', None) |
|
|||
222 | if callable(callback): |
|
|||
223 | return callback(created_by=created_by, **user_dict) |
|
|||
224 |
|
||||
225 | return 0 |
|
|||
226 |
|
||||
227 |
|
||||
228 | def log_delete_repository(repository_dict, deleted_by, **kwargs): |
|
|||
229 | """ |
|
|||
230 | Post delete repository Hook. |
|
|||
231 |
|
||||
232 | :param repository: dict dump of repository object |
|
|||
233 | :param deleted_by: username who deleted the repository |
|
|||
234 |
|
||||
235 | available keys of repository_dict: |
|
|||
236 |
|
||||
237 | 'repo_type', |
|
|||
238 | 'description', |
|
|||
239 | 'private', |
|
|||
240 | 'created_on', |
|
|||
241 | 'enable_downloads', |
|
|||
242 | 'repo_id', |
|
|||
243 | 'owner_id', |
|
|||
244 | 'enable_statistics', |
|
|||
245 | 'clone_uri', |
|
|||
246 | 'fork_id', |
|
|||
247 | 'group_id', |
|
|||
248 | 'repo_name' |
|
|||
249 |
|
||||
250 | """ |
|
|||
251 | from kallithea import EXTENSIONS |
|
|||
252 | callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None) |
|
|||
253 | if callable(callback): |
|
|||
254 | kw = {} |
|
|||
255 | kw.update(repository_dict) |
|
|||
256 | kw.update({'deleted_by': deleted_by, |
|
|||
257 | 'deleted_on': time.time()}) |
|
|||
258 | kw.update(kwargs) |
|
|||
259 | return callback(**kw) |
|
|||
260 |
|
||||
261 | return 0 |
|
|||
262 |
|
||||
263 |
|
||||
264 | def log_delete_user(user_dict, deleted_by, **kwargs): |
|
|||
265 | """ |
|
|||
266 | Post delete user Hook. |
|
|||
267 |
|
||||
268 | :param user_dict: dict dump of user object |
|
|||
269 |
|
||||
270 | available keys for user_dict: |
|
|||
271 |
|
||||
272 | 'username', |
|
|||
273 | 'full_name_or_username', |
|
|||
274 | 'full_contact', |
|
|||
275 | 'user_id', |
|
|||
276 | 'name', |
|
|||
277 | 'firstname', |
|
|||
278 | 'short_contact', |
|
|||
279 | 'admin', |
|
|||
280 | 'lastname', |
|
|||
281 | 'ip_addresses', |
|
|||
282 | 'ldap_dn', |
|
|||
283 | 'email', |
|
|||
284 | 'api_key', |
|
|||
285 | 'last_login', |
|
|||
286 | 'full_name', |
|
|||
287 | 'active', |
|
|||
288 | 'password', |
|
|||
289 | 'emails', |
|
|||
290 |
|
||||
291 | """ |
|
|||
292 | from kallithea import EXTENSIONS |
|
|||
293 | callback = getattr(EXTENSIONS, 'DELETE_USER_HOOK', None) |
|
|||
294 | if callable(callback): |
|
|||
295 | return callback(deleted_by=deleted_by, **user_dict) |
|
|||
296 |
|
||||
297 | return 0 |
|
|||
298 |
|
||||
299 |
|
||||
300 | def _hook_environment(repo_path): |
|
|||
301 | """ |
|
106 | """ | |
302 | Create a light-weight environment for stand-alone scripts and return an UI and the |
|
107 | Create a light-weight environment for stand-alone scripts and return an UI and the | |
303 | db repository. |
|
108 | db repository. | |
@@ -306,40 +111,32 b' def _hook_environment(repo_path):' | |||||
306 | they thus need enough info to be able to create an app environment and |
|
111 | they thus need enough info to be able to create an app environment and | |
307 | connect to the database. |
|
112 | connect to the database. | |
308 | """ |
|
113 | """ | |
309 | import paste.deploy |
|
|||
310 | import kallithea.config.middleware |
|
|||
311 |
|
||||
312 | extras = get_hook_environment() |
|
114 | extras = get_hook_environment() | |
313 |
|
115 | |||
314 | path_to_ini_file = extras['config'] |
|
116 | path_to_ini_file = extras['config'] | |
315 |
|
|
117 | config = paste.deploy.appconfig('config:' + path_to_ini_file) | |
316 | #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging |
|
118 | #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging | |
317 |
kallithea.config. |
|
119 | kallithea.config.application.make_app(config.global_conf, **config.local_conf) | |
318 |
|
120 | |||
319 | # fix if it's not a bare repo |
|
121 | # fix if it's not a bare repo | |
320 | if repo_path.endswith(os.sep + '.git'): |
|
122 | if repo_path.endswith(os.sep + '.git'): | |
321 | repo_path = repo_path[:-5] |
|
123 | repo_path = repo_path[:-5] | |
322 |
|
124 | |||
323 | repo = Repository.get_by_full_path(repo_path) |
|
125 | repo = db.Repository.get_by_full_path(repo_path) | |
324 | if not repo: |
|
126 | if not repo: | |
325 | raise OSError('Repository %s not found in database' % repo_path) |
|
127 | raise OSError('Repository %s not found in database' % repo_path) | |
326 |
|
128 | |||
327 | baseui = make_ui() |
|
129 | return repo | |
328 | return baseui, repo |
|
|||
329 |
|
130 | |||
330 |
|
131 | |||
331 |
def |
|
132 | def post_receive(repo_path, git_stdin_lines): | |
332 |
"""Called from Git p |
|
133 | """Called from Git post-receive hook. | |
333 | # Currently unused. TODO: remove? |
|
134 | The returned value is used as hook exit code and must be 0. | |
334 | return 0 |
|
135 | """ | |
335 |
|
||||
336 |
|
||||
337 | def handle_git_post_receive(repo_path, git_stdin_lines): |
|
|||
338 | """Called from Git post-receive hook""" |
|
|||
339 | try: |
|
136 | try: | |
340 |
|
|
137 | repo = _git_hook_environment(repo_path) | |
341 | except HookEnvironmentError as e: |
|
138 | except HookEnvironmentError as e: | |
342 |
sys.stderr.write("Skipping Kallithea Git post-rec |
|
139 | sys.stderr.write("Skipping Kallithea Git post-receive hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e)) | |
343 | return 0 |
|
140 | return 0 | |
344 |
|
141 | |||
345 | # the post push hook should never use the cached instance |
|
142 | # the post push hook should never use the cached instance | |
@@ -391,14 +188,16 b' def handle_git_post_receive(repo_path, g' | |||||
391 | elif _type == 'tags': |
|
188 | elif _type == 'tags': | |
392 | git_revs += ['tag=>%s' % push_ref['name']] |
|
189 | git_revs += ['tag=>%s' % push_ref['name']] | |
393 |
|
190 | |||
394 | process_pushed_raw_ids(git_revs) |
|
191 | hooks.process_pushed_raw_ids(git_revs) | |
395 |
|
192 | |||
396 | return 0 |
|
193 | return 0 | |
397 |
|
194 | |||
398 |
|
195 | |||
399 | # Almost exactly like Mercurial contrib/hg-ssh: |
|
196 | # Almost exactly like Mercurial contrib/hg-ssh: | |
400 | def rejectpush(ui, **kwargs): |
|
197 | def rejectpush(ui, **kwargs): | |
401 |
"""Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos |
|
198 | """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos. | |
|
199 | Return value 1 will make the hook fail and reject the push. | |||
|
200 | """ | |||
402 | ex = get_hook_environment() |
|
201 | ex = get_hook_environment() | |
403 | ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository)) |
|
202 | ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository)) | |
404 | return 1 |
|
203 | return 1 |
@@ -28,82 +28,57 b' import tg' | |||||
28 | from alembic.migration import MigrationContext |
|
28 | from alembic.migration import MigrationContext | |
29 | from alembic.script.base import ScriptDirectory |
|
29 | from alembic.script.base import ScriptDirectory | |
30 | from sqlalchemy import create_engine |
|
30 | from sqlalchemy import create_engine | |
31 | from tg.configuration import AppConfig |
|
31 | from tg import FullStackApplicationConfigurator | |
32 | from tg.support.converters import asbool |
|
|||
33 |
|
32 | |||
34 | import kallithea.lib.locale |
|
33 | import kallithea.lib.locales | |
35 | import kallithea.model.base |
|
34 | import kallithea.model.base | |
36 | import kallithea.model.meta |
|
35 | import kallithea.model.meta | |
37 |
from kallithea.lib import celery |
|
36 | from kallithea.lib import celery_app | |
38 | from kallithea.lib.middleware.https_fixup import HttpsFixup |
|
37 | from kallithea.lib.utils import load_extensions, set_app_settings, set_indexer_config, set_vcs_config | |
39 | from kallithea.lib.middleware.permanent_repo_url import PermanentRepoUrl |
|
38 | from kallithea.lib.utils2 import asbool, check_git_version | |
40 | from kallithea.lib.middleware.simplegit import SimpleGit |
|
|||
41 | from kallithea.lib.middleware.simplehg import SimpleHg |
|
|||
42 | from kallithea.lib.middleware.wrapper import RequestWrapper |
|
|||
43 | from kallithea.lib.utils import check_git_version, load_rcextensions, set_app_settings, set_indexer_config, set_vcs_config |
|
|||
44 | from kallithea.lib.utils2 import str2bool |
|
|||
45 | from kallithea.model import db |
|
39 | from kallithea.model import db | |
46 |
|
40 | |||
47 |
|
41 | |||
48 | log = logging.getLogger(__name__) |
|
42 | log = logging.getLogger(__name__) | |
49 |
|
43 | |||
50 |
|
44 | |||
51 | class KallitheaAppConfig(AppConfig): |
|
45 | base_config = FullStackApplicationConfigurator() | |
52 | # Note: AppConfig has a misleading name, as it's not the application |
|
|||
53 | # configuration, but the application configurator. The AppConfig values are |
|
|||
54 | # used as a template to create the actual configuration, which might |
|
|||
55 | # overwrite or extend the one provided by the configurator template. |
|
|||
56 |
|
46 | |||
57 | # To make it clear, AppConfig creates the config and sets into it the same |
|
47 | base_config.update_blueprint({ | |
58 | # values that AppConfig itself has. Then the values from the config file and |
|
48 | 'package': kallithea, | |
59 | # gearbox options are loaded and merged into the configuration. Then an |
|
|||
60 | # after_init_config(conf) method of AppConfig is called for any change that |
|
|||
61 | # might depend on options provided by configuration files. |
|
|||
62 |
|
49 | |||
63 | def __init__(self): |
|
50 | # Rendering Engines Configuration | |
64 | super(KallitheaAppConfig, self).__init__() |
|
51 | 'renderers': [ | |
65 |
|
52 | 'json', | ||
66 | self['package'] = kallithea |
|
53 | 'mako', | |
|
54 | ], | |||
|
55 | 'default_renderer': 'mako', | |||
|
56 | 'use_dotted_templatenames': False, | |||
67 |
|
57 | |||
68 | self['prefer_toscawidgets2'] = False |
|
58 | # Configure Sessions, store data as JSON to avoid pickle security issues | |
69 | self['use_toscawidgets'] = False |
|
59 | 'session.enabled': True, | |
70 |
|
60 | 'session.data_serializer': 'json', | ||
71 | self['renderers'] = [] |
|
|||
72 |
|
||||
73 | # Enable json in expose |
|
|||
74 | self['renderers'].append('json') |
|
|||
75 |
|
61 | |||
76 | # Configure template rendering |
|
62 | # Configure the base SQLALchemy Setup | |
77 | self['renderers'].append('mako') |
|
63 | 'use_sqlalchemy': True, | |
78 | self['default_renderer'] = 'mako' |
|
64 | 'model': kallithea.model.base, | |
79 | self['use_dotted_templatenames'] = False |
|
65 | 'DBSession': kallithea.model.meta.Session, | |
80 |
|
66 | |||
81 | # Configure Sessions, store data as JSON to avoid pickle security issues |
|
67 | # Configure App without an authentication backend. | |
82 | self['session.enabled'] = True |
|
68 | 'auth_backend': None, | |
83 | self['session.data_serializer'] = 'json' |
|
|||
84 |
|
||||
85 | # Configure the base SQLALchemy Setup |
|
|||
86 | self['use_sqlalchemy'] = True |
|
|||
87 | self['model'] = kallithea.model.base |
|
|||
88 | self['DBSession'] = kallithea.model.meta.Session |
|
|||
89 |
|
69 | |||
90 | # Configure App without an authentication backend. |
|
70 | # Use custom error page for these errors. By default, Turbogears2 does not add | |
91 | self['auth_backend'] = None |
|
71 | # 400 in this list. | |
92 |
|
72 | # Explicitly listing all is considered more robust than appending to defaults, | ||
93 | # Use custom error page for these errors. By default, Turbogears2 does not add |
|
73 | # in light of possible future framework changes. | |
94 | # 400 in this list. |
|
74 | 'errorpage.status_codes': [400, 401, 403, 404], | |
95 | # Explicitly listing all is considered more robust than appending to defaults, |
|
|||
96 | # in light of possible future framework changes. |
|
|||
97 | self['errorpage.status_codes'] = [400, 401, 403, 404] |
|
|||
98 |
|
75 | |||
99 |
|
|
76 | # Disable transaction manager -- currently Kallithea takes care of transactions itself | |
100 |
|
|
77 | 'tm.enabled': False, | |
101 |
|
78 | |||
102 |
|
|
79 | # Set the default i18n source language so TG doesn't search beyond 'en' in Accept-Language. | |
103 |
|
|
80 | 'i18n.lang': 'en', | |
104 |
|
81 | }) | ||
105 |
|
||||
106 | base_config = KallitheaAppConfig() |
|
|||
107 |
|
82 | |||
108 | # DebugBar, a debug toolbar for TurboGears2. |
|
83 | # DebugBar, a debug toolbar for TurboGears2. | |
109 | # (https://github.com/TurboGears/tgext.debugbar) |
|
84 | # (https://github.com/TurboGears/tgext.debugbar) | |
@@ -111,20 +86,20 b' base_config = KallitheaAppConfig()' | |||||
111 | # 'debug = true' (not in production!) |
|
86 | # 'debug = true' (not in production!) | |
112 | # See the Kallithea documentation for more information. |
|
87 | # See the Kallithea documentation for more information. | |
113 | try: |
|
88 | try: | |
|
89 | import kajiki # only to check its existence | |||
114 | from tgext.debugbar import enable_debugbar |
|
90 | from tgext.debugbar import enable_debugbar | |
115 | import kajiki # only to check its existence |
|
|||
116 | assert kajiki |
|
91 | assert kajiki | |
117 | except ImportError: |
|
92 | except ImportError: | |
118 | pass |
|
93 | pass | |
119 | else: |
|
94 | else: | |
120 |
base_config |
|
95 | base_config.get_blueprint_value('renderers').append('kajiki') | |
121 | enable_debugbar(base_config) |
|
96 | enable_debugbar(base_config) | |
122 |
|
97 | |||
123 |
|
98 | |||
124 | def setup_configuration(app): |
|
99 | def setup_configuration(app): | |
125 | config = app.config |
|
100 | config = app.config | |
126 |
|
101 | |||
127 | if not kallithea.lib.locale.current_locale_is_valid(): |
|
102 | if not kallithea.lib.locales.current_locale_is_valid(): | |
128 | log.error("Terminating ...") |
|
103 | log.error("Terminating ...") | |
129 | sys.exit(1) |
|
104 | sys.exit(1) | |
130 |
|
105 | |||
@@ -134,7 +109,7 b' def setup_configuration(app):' | |||||
134 | mercurial.encoding.encoding = hgencoding |
|
109 | mercurial.encoding.encoding = hgencoding | |
135 |
|
110 | |||
136 | if config.get('ignore_alembic_revision', False): |
|
111 | if config.get('ignore_alembic_revision', False): | |
137 | log.warn('database alembic revision checking is disabled') |
|
112 | log.warning('database alembic revision checking is disabled') | |
138 | else: |
|
113 | else: | |
139 | dbconf = config['sqlalchemy.url'] |
|
114 | dbconf = config['sqlalchemy.url'] | |
140 | alembic_cfg = alembic.config.Config() |
|
115 | alembic_cfg = alembic.config.Config() | |
@@ -160,11 +135,11 b' def setup_configuration(app):' | |||||
160 | # store some globals into kallithea |
|
135 | # store some globals into kallithea | |
161 | kallithea.DEFAULT_USER_ID = db.User.get_default_user().user_id |
|
136 | kallithea.DEFAULT_USER_ID = db.User.get_default_user().user_id | |
162 |
|
137 | |||
163 |
if s |
|
138 | if asbool(config.get('use_celery')) and not kallithea.CELERY_APP.finalized: | |
164 |
kallithea.CELERY_APP |
|
139 | kallithea.CELERY_APP.config_from_object(celery_app.make_celery_config(config)) | |
165 | kallithea.CONFIG = config |
|
140 | kallithea.CONFIG = config | |
166 |
|
141 | |||
167 |
load_ |
|
142 | load_extensions(root_path=config['here']) | |
168 |
|
143 | |||
169 | set_app_settings(config) |
|
144 | set_app_settings(config) | |
170 |
|
145 | |||
@@ -188,27 +163,3 b' def setup_configuration(app):' | |||||
188 |
|
163 | |||
189 |
|
164 | |||
190 | tg.hooks.register('configure_new_app', setup_configuration) |
|
165 | tg.hooks.register('configure_new_app', setup_configuration) | |
191 |
|
||||
192 |
|
||||
193 | def setup_application(app): |
|
|||
194 | config = app.config |
|
|||
195 |
|
||||
196 | # we want our low level middleware to get to the request ASAP. We don't |
|
|||
197 | # need any stack middleware in them - especially no StatusCodeRedirect buffering |
|
|||
198 | app = SimpleHg(app, config) |
|
|||
199 | app = SimpleGit(app, config) |
|
|||
200 |
|
||||
201 | # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy |
|
|||
202 | if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']): |
|
|||
203 | app = HttpsFixup(app, config) |
|
|||
204 |
|
||||
205 | app = PermanentRepoUrl(app, config) |
|
|||
206 |
|
||||
207 | # Optional and undocumented wrapper - gives more verbose request/response logging, but has a slight overhead |
|
|||
208 | if str2bool(config.get('use_wsgi_wrapper')): |
|
|||
209 | app = RequestWrapper(app, config) |
|
|||
210 |
|
||||
211 | return app |
|
|||
212 |
|
||||
213 |
|
||||
214 | tg.hooks.register('before_config', setup_application) |
|
@@ -14,26 +14,46 b'' | |||||
14 | """WSGI middleware initialization for the Kallithea application.""" |
|
14 | """WSGI middleware initialization for the Kallithea application.""" | |
15 |
|
15 | |||
16 | from kallithea.config.app_cfg import base_config |
|
16 | from kallithea.config.app_cfg import base_config | |
17 | from kallithea.config.environment import load_environment |
|
17 | from kallithea.config.middleware.https_fixup import HttpsFixup | |
|
18 | from kallithea.config.middleware.permanent_repo_url import PermanentRepoUrl | |||
|
19 | from kallithea.config.middleware.simplegit import SimpleGit | |||
|
20 | from kallithea.config.middleware.simplehg import SimpleHg | |||
|
21 | from kallithea.config.middleware.wrapper import RequestWrapper | |||
|
22 | from kallithea.lib.utils2 import asbool | |||
18 |
|
23 | |||
19 |
|
24 | |||
20 | __all__ = ['make_app'] |
|
25 | __all__ = ['make_app'] | |
21 |
|
26 | |||
22 | # Use base_config to setup the necessary PasteDeploy application factory. |
|
27 | ||
23 | # make_base_app will wrap the TurboGears2 app with all the middleware it needs. |
|
28 | def wrap_app(app): | |
24 | make_base_app = base_config.setup_tg_wsgi_app(load_environment) |
|
29 | """Wrap the TG WSGI application in Kallithea middleware""" | |
|
30 | config = app.config | |||
|
31 | ||||
|
32 | # we want our low level middleware to get to the request ASAP. We don't | |||
|
33 | # need any stack middleware in them - especially no StatusCodeRedirect buffering | |||
|
34 | app = SimpleHg(app, config) | |||
|
35 | app = SimpleGit(app, config) | |||
|
36 | ||||
|
37 | # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy | |||
|
38 | if any(asbool(config.get(x)) for x in ['url_scheme_variable', 'force_https', 'use_htsts']): | |||
|
39 | app = HttpsFixup(app, config) | |||
|
40 | ||||
|
41 | app = PermanentRepoUrl(app, config) | |||
|
42 | ||||
|
43 | # Optional and undocumented wrapper - gives more verbose request/response logging, but has a slight overhead | |||
|
44 | if asbool(config.get('use_wsgi_wrapper')): | |||
|
45 | app = RequestWrapper(app, config) | |||
|
46 | ||||
|
47 | return app | |||
25 |
|
48 | |||
26 |
|
49 | |||
27 |
def make_app(global_conf, |
|
50 | def make_app(global_conf, **app_conf): | |
28 | """ |
|
51 | """ | |
29 | Set up Kallithea with the settings found in the PasteDeploy configuration |
|
52 | Set up Kallithea with the settings found in the PasteDeploy configuration | |
30 | file used. |
|
53 | file used. | |
31 |
|
54 | |||
32 | :param global_conf: The global settings for Kallithea (those |
|
55 | :param global_conf: The global settings for Kallithea (those | |
33 | defined under the ``[DEFAULT]`` section). |
|
56 | defined under the ``[DEFAULT]`` section). | |
34 | :type global_conf: dict |
|
|||
35 | :param full_stack: Should the whole TurboGears2 stack be set up? |
|
|||
36 | :type full_stack: str or bool |
|
|||
37 | :return: The Kallithea application with all the relevant middleware |
|
57 | :return: The Kallithea application with all the relevant middleware | |
38 | loaded. |
|
58 | loaded. | |
39 |
|
59 | |||
@@ -44,4 +64,5 b' def make_app(global_conf, full_stack=Tru' | |||||
44 | """ |
|
64 | """ | |
45 | assert app_conf.get('sqlalchemy.url') # must be called with a Kallithea .ini file, which for example must have this config option |
|
65 | assert app_conf.get('sqlalchemy.url') # must be called with a Kallithea .ini file, which for example must have this config option | |
46 | assert global_conf.get('here') and global_conf.get('__file__') # app config should be initialized the paste way ... |
|
66 | assert global_conf.get('here') and global_conf.get('__file__') # app config should be initialized the paste way ... | |
47 | return make_base_app(global_conf, full_stack=full_stack, **app_conf) |
|
67 | ||
|
68 | return base_config.make_wsgi_app(global_conf, app_conf, wrap_app=wrap_app) |
1 | NO CONTENT: file renamed from kallithea/lib/middleware/__init__.py to kallithea/config/middleware/__init__.py |
|
NO CONTENT: file renamed from kallithea/lib/middleware/__init__.py to kallithea/config/middleware/__init__.py |
@@ -12,8 +12,8 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 | """ |
|
14 | """ | |
15 |
kallithea. |
|
15 | kallithea.config.middleware.https_fixup | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | middleware to handle https correctly |
|
18 | middleware to handle https correctly | |
19 |
|
19 | |||
@@ -26,7 +26,8 b' Original author and date, and relevant c' | |||||
26 | """ |
|
26 | """ | |
27 |
|
27 | |||
28 |
|
28 | |||
29 | from kallithea.lib.utils2 import str2bool |
|
29 | import kallithea | |
|
30 | from kallithea.lib.utils2 import asbool | |||
30 |
|
31 | |||
31 |
|
32 | |||
32 | class HttpsFixup(object): |
|
33 | class HttpsFixup(object): | |
@@ -37,11 +38,11 b' class HttpsFixup(object):' | |||||
37 |
|
38 | |||
38 | def __call__(self, environ, start_response): |
|
39 | def __call__(self, environ, start_response): | |
39 | self.__fixup(environ) |
|
40 | self.__fixup(environ) | |
40 |
debug = s |
|
41 | debug = asbool(self.config.get('debug')) | |
41 | is_ssl = environ['wsgi.url_scheme'] == 'https' |
|
42 | is_ssl = environ['wsgi.url_scheme'] == 'https' | |
42 |
|
43 | |||
43 | def custom_start_response(status, headers, exc_info=None): |
|
44 | def custom_start_response(status, headers, exc_info=None): | |
44 |
if is_ssl and s |
|
45 | if is_ssl and asbool(self.config.get('use_htsts')) and not debug: | |
45 | headers.append(('Strict-Transport-Security', |
|
46 | headers.append(('Strict-Transport-Security', | |
46 | 'max-age=8640000; includeSubDomains')) |
|
47 | 'max-age=8640000; includeSubDomains')) | |
47 | return start_response(status, headers, exc_info) |
|
48 | return start_response(status, headers, exc_info) | |
@@ -54,20 +55,17 b' class HttpsFixup(object):' | |||||
54 | middleware you should set this header inside your |
|
55 | middleware you should set this header inside your | |
55 | proxy ie. nginx, apache etc. |
|
56 | proxy ie. nginx, apache etc. | |
56 | """ |
|
57 | """ | |
57 | # DETECT PROTOCOL ! |
|
58 | proto = None | |
58 | if 'HTTP_X_URL_SCHEME' in environ: |
|
|||
59 | proto = environ.get('HTTP_X_URL_SCHEME') |
|
|||
60 | elif 'HTTP_X_FORWARDED_SCHEME' in environ: |
|
|||
61 | proto = environ.get('HTTP_X_FORWARDED_SCHEME') |
|
|||
62 | elif 'HTTP_X_FORWARDED_PROTO' in environ: |
|
|||
63 | proto = environ.get('HTTP_X_FORWARDED_PROTO') |
|
|||
64 | else: |
|
|||
65 | proto = 'http' |
|
|||
66 | org_proto = proto |
|
|||
67 |
|
59 | |||
68 | # if we have force, just override |
|
60 | # if we have force, just override | |
69 |
if s |
|
61 | if asbool(self.config.get('force_https')): | |
70 | proto = 'https' |
|
62 | proto = 'https' | |
|
63 | else: | |||
|
64 | # get protocol from configured WSGI environment variable | |||
|
65 | url_scheme_variable = kallithea.CONFIG.get('url_scheme_variable') | |||
|
66 | if url_scheme_variable: | |||
|
67 | proto = environ.get(url_scheme_variable) | |||
71 |
|
68 | |||
72 | environ['wsgi.url_scheme'] = proto |
|
69 | if proto: | |
73 |
environ['wsgi._org_proto'] = |
|
70 | environ['wsgi._org_proto'] = environ.get('wsgi.url_scheme') | |
|
71 | environ['wsgi.url_scheme'] = proto |
@@ -12,8 +12,8 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 | """ |
|
14 | """ | |
15 |
kallithea. |
|
15 | kallithea.config.middleware.permanent_repo_url | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | middleware to handle permanent repo URLs, replacing PATH_INFO '/_123/yada' with |
|
18 | middleware to handle permanent repo URLs, replacing PATH_INFO '/_123/yada' with | |
19 | '/name/of/repo/yada' after looking 123 up in the database. |
|
19 | '/name/of/repo/yada' after looking 123 up in the database. |
@@ -12,8 +12,8 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 | """ |
|
14 | """ | |
15 |
kallithea. |
|
15 | kallithea.config.middleware.pygrack | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | Python implementation of git-http-backend's Smart HTTP protocol |
|
18 | Python implementation of git-http-backend's Smart HTTP protocol | |
19 |
|
19 | |||
@@ -30,11 +30,13 b' import os' | |||||
30 | import socket |
|
30 | import socket | |
31 | import traceback |
|
31 | import traceback | |
32 |
|
32 | |||
|
33 | from dulwich.server import update_server_info | |||
|
34 | from dulwich.web import GunzipFilter, LimitedInputFilter | |||
33 | from webob import Request, Response, exc |
|
35 | from webob import Request, Response, exc | |
34 |
|
36 | |||
35 | import kallithea |
|
37 | import kallithea | |
36 | from kallithea.lib.utils2 import ascii_bytes |
|
38 | from kallithea.lib.utils2 import ascii_bytes | |
37 | from kallithea.lib.vcs import subprocessio |
|
39 | from kallithea.lib.vcs import get_repo, subprocessio | |
38 |
|
40 | |||
39 |
|
41 | |||
40 | log = logging.getLogger(__name__) |
|
42 | log = logging.getLogger(__name__) | |
@@ -168,8 +170,6 b' class GitRepository(object):' | |||||
168 | if git_command in ['git-receive-pack']: |
|
170 | if git_command in ['git-receive-pack']: | |
169 | # updating refs manually after each push. |
|
171 | # updating refs manually after each push. | |
170 | # Needed for pre-1.7.0.4 git clients using regular HTTP mode. |
|
172 | # Needed for pre-1.7.0.4 git clients using regular HTTP mode. | |
171 | from kallithea.lib.vcs import get_repo |
|
|||
172 | from dulwich.server import update_server_info |
|
|||
173 | repo = get_repo(self.content_path) |
|
173 | repo = get_repo(self.content_path) | |
174 | if repo: |
|
174 | if repo: | |
175 | update_server_info(repo._repo) |
|
175 | update_server_info(repo._repo) | |
@@ -223,6 +223,5 b' class GitDirectory(object):' | |||||
223 |
|
223 | |||
224 |
|
224 | |||
225 | def make_wsgi_app(repo_name, repo_root): |
|
225 | def make_wsgi_app(repo_name, repo_root): | |
226 | from dulwich.web import LimitedInputFilter, GunzipFilter |
|
|||
227 | app = GitDirectory(repo_root, repo_name) |
|
226 | app = GitDirectory(repo_root, repo_name) | |
228 | return GunzipFilter(LimitedInputFilter(app)) |
|
227 | return GunzipFilter(LimitedInputFilter(app)) |
@@ -12,8 +12,8 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 | """ |
|
14 | """ | |
15 |
kallithea. |
|
15 | kallithea.config.middleware.simplegit | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | SimpleGit middleware for handling Git protocol requests (push/clone etc.) |
|
18 | SimpleGit middleware for handling Git protocol requests (push/clone etc.) | |
19 | It's implemented with basic auth function |
|
19 | It's implemented with basic auth function | |
@@ -31,11 +31,9 b' Original author and date, and relevant c' | |||||
31 | import logging |
|
31 | import logging | |
32 | import re |
|
32 | import re | |
33 |
|
33 | |||
34 | from kallithea.lib.base import BaseVCSController, get_path_info |
|
34 | from kallithea.config.middleware.pygrack import make_wsgi_app | |
35 |
from kallithea. |
|
35 | from kallithea.controllers import base | |
36 |
from kallithea.lib |
|
36 | from kallithea.lib import hooks | |
37 | from kallithea.lib.utils import make_ui |
|
|||
38 | from kallithea.model.db import Repository |
|
|||
39 |
|
37 | |||
40 |
|
38 | |||
41 | log = logging.getLogger(__name__) |
|
39 | log = logging.getLogger(__name__) | |
@@ -50,13 +48,13 b' cmd_mapping = {' | |||||
50 | } |
|
48 | } | |
51 |
|
49 | |||
52 |
|
50 | |||
53 | class SimpleGit(BaseVCSController): |
|
51 | class SimpleGit(base.BaseVCSController): | |
54 |
|
52 | |||
55 | scm_alias = 'git' |
|
53 | scm_alias = 'git' | |
56 |
|
54 | |||
57 | @classmethod |
|
55 | @classmethod | |
58 | def parse_request(cls, environ): |
|
56 | def parse_request(cls, environ): | |
59 | path_info = get_path_info(environ) |
|
57 | path_info = base.get_path_info(environ) | |
60 | m = GIT_PROTO_PAT.match(path_info) |
|
58 | m = GIT_PROTO_PAT.match(path_info) | |
61 | if m is None: |
|
59 | if m is None: | |
62 | return None |
|
60 | return None | |
@@ -86,11 +84,8 b' class SimpleGit(BaseVCSController):' | |||||
86 | if (parsed_request.cmd == 'info/refs' and |
|
84 | if (parsed_request.cmd == 'info/refs' and | |
87 | parsed_request.service == 'git-upload-pack' |
|
85 | parsed_request.service == 'git-upload-pack' | |
88 | ): |
|
86 | ): | |
89 | baseui = make_ui() |
|
87 | # Run hooks like Mercurial outgoing.kallithea_pull_action does | |
90 | repo = Repository.get_by_repo_name(parsed_request.repo_name) |
|
88 | hooks.log_pull_action() | |
91 | scm_repo = repo.scm_instance |
|
|||
92 | # Run hooks, like Mercurial outgoing.pull_logger does |
|
|||
93 | log_pull_action(ui=baseui, repo=scm_repo._repo) |
|
|||
94 | # Note: push hooks are handled by post-receive hook |
|
89 | # Note: push hooks are handled by post-receive hook | |
95 |
|
90 | |||
96 | return pygrack_app(environ, start_response) |
|
91 | return pygrack_app(environ, start_response) |
@@ -12,8 +12,8 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 | """ |
|
14 | """ | |
15 |
kallithea. |
|
15 | kallithea.config.middleware.simplehg | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | SimpleHg middleware for handling Mercurial protocol requests (push/clone etc.). |
|
18 | SimpleHg middleware for handling Mercurial protocol requests (push/clone etc.). | |
19 | It's implemented with basic auth function |
|
19 | It's implemented with basic auth function | |
@@ -34,7 +34,7 b' import urllib.parse' | |||||
34 |
|
34 | |||
35 | import mercurial.hgweb |
|
35 | import mercurial.hgweb | |
36 |
|
36 | |||
37 | from kallithea.lib.base import BaseVCSController, get_path_info |
|
37 | from kallithea.controllers import base | |
38 | from kallithea.lib.utils import make_ui |
|
38 | from kallithea.lib.utils import make_ui | |
39 | from kallithea.lib.utils2 import safe_bytes |
|
39 | from kallithea.lib.utils2 import safe_bytes | |
40 |
|
40 | |||
@@ -91,7 +91,7 b' cmd_mapping = {' | |||||
91 | } |
|
91 | } | |
92 |
|
92 | |||
93 |
|
93 | |||
94 | class SimpleHg(BaseVCSController): |
|
94 | class SimpleHg(base.BaseVCSController): | |
95 |
|
95 | |||
96 | scm_alias = 'hg' |
|
96 | scm_alias = 'hg' | |
97 |
|
97 | |||
@@ -100,7 +100,7 b' class SimpleHg(BaseVCSController):' | |||||
100 | http_accept = environ.get('HTTP_ACCEPT', '') |
|
100 | http_accept = environ.get('HTTP_ACCEPT', '') | |
101 | if not http_accept.startswith('application/mercurial'): |
|
101 | if not http_accept.startswith('application/mercurial'): | |
102 | return None |
|
102 | return None | |
103 | path_info = get_path_info(environ) |
|
103 | path_info = base.get_path_info(environ) | |
104 | if not path_info.startswith('/'): # it must! |
|
104 | if not path_info.startswith('/'): # it must! | |
105 | return None |
|
105 | return None | |
106 |
|
106 |
@@ -12,8 +12,8 b'' | |||||
12 | # You should have received a copy of the GNU General Public License |
|
12 | # You should have received a copy of the GNU General Public License | |
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 | """ |
|
14 | """ | |
15 |
kallithea. |
|
15 | kallithea.config.middleware.wrapper | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 |
|
17 | |||
18 | Wrap app to measure request and response time ... all the way to the response |
|
18 | Wrap app to measure request and response time ... all the way to the response | |
19 | WSGI iterator has been closed. |
|
19 | WSGI iterator has been closed. | |
@@ -29,7 +29,7 b' Original author and date, and relevant c' | |||||
29 | import logging |
|
29 | import logging | |
30 | import time |
|
30 | import time | |
31 |
|
31 | |||
32 | from kallithea.lib.base import _get_ip_addr, get_path_info |
|
32 | from kallithea.controllers import base | |
33 |
|
33 | |||
34 |
|
34 | |||
35 | log = logging.getLogger(__name__) |
|
35 | log = logging.getLogger(__name__) | |
@@ -91,8 +91,8 b' class RequestWrapper(object):' | |||||
91 | def __call__(self, environ, start_response): |
|
91 | def __call__(self, environ, start_response): | |
92 | meter = Meter(start_response) |
|
92 | meter = Meter(start_response) | |
93 | description = "Request from %s for %s" % ( |
|
93 | description = "Request from %s for %s" % ( | |
94 |
|
|
94 | base.get_ip_addr(environ), | |
95 | get_path_info(environ), |
|
95 | base.get_path_info(environ), | |
96 | ) |
|
96 | ) | |
97 | log.info("%s received", description) |
|
97 | log.info("%s received", description) | |
98 | try: |
|
98 | try: |
@@ -36,12 +36,12 b' from whoosh import query' | |||||
36 | from whoosh.qparser.dateparse import DateParserPlugin |
|
36 | from whoosh.qparser.dateparse import DateParserPlugin | |
37 | from whoosh.qparser.default import QueryParser |
|
37 | from whoosh.qparser.default import QueryParser | |
38 |
|
38 | |||
|
39 | from kallithea.controllers import base | |||
39 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired |
|
40 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired | |
40 | from kallithea.lib.base import BaseController, render |
|
|||
41 | from kallithea.lib.indexers import JOURNAL_SCHEMA |
|
41 | from kallithea.lib.indexers import JOURNAL_SCHEMA | |
42 | from kallithea.lib.page import Page |
|
42 | from kallithea.lib.page import Page | |
43 | from kallithea.lib.utils2 import remove_prefix, remove_suffix, safe_int |
|
43 | from kallithea.lib.utils2 import remove_prefix, remove_suffix, safe_int | |
44 |
from kallithea.model |
|
44 | from kallithea.model import db | |
45 |
|
45 | |||
46 |
|
46 | |||
47 | log = logging.getLogger(__name__) |
|
47 | log = logging.getLogger(__name__) | |
@@ -77,15 +77,15 b' def _journal_filter(user_log, search_ter' | |||||
77 | def get_filterion(field, val, term): |
|
77 | def get_filterion(field, val, term): | |
78 |
|
78 | |||
79 | if field == 'repository': |
|
79 | if field == 'repository': | |
80 | field = getattr(UserLog, 'repository_name') |
|
80 | field = getattr(db.UserLog, 'repository_name') | |
81 | elif field == 'ip': |
|
81 | elif field == 'ip': | |
82 | field = getattr(UserLog, 'user_ip') |
|
82 | field = getattr(db.UserLog, 'user_ip') | |
83 | elif field == 'date': |
|
83 | elif field == 'date': | |
84 | field = getattr(UserLog, 'action_date') |
|
84 | field = getattr(db.UserLog, 'action_date') | |
85 | elif field == 'username': |
|
85 | elif field == 'username': | |
86 | field = getattr(UserLog, 'username') |
|
86 | field = getattr(db.UserLog, 'username') | |
87 | else: |
|
87 | else: | |
88 | field = getattr(UserLog, field) |
|
88 | field = getattr(db.UserLog, field) | |
89 | log.debug('filter field: %s val=>%s', field, val) |
|
89 | log.debug('filter field: %s val=>%s', field, val) | |
90 |
|
90 | |||
91 | # sql filtering |
|
91 | # sql filtering | |
@@ -102,6 +102,7 b' def _journal_filter(user_log, search_ter' | |||||
102 | if not isinstance(qry, query.And): |
|
102 | if not isinstance(qry, query.And): | |
103 | qry = [qry] |
|
103 | qry = [qry] | |
104 | for term in qry: |
|
104 | for term in qry: | |
|
105 | assert term is not None, term | |||
105 | field = term.fieldname |
|
106 | field = term.fieldname | |
106 | val = (term.text if not isinstance(term, query.DateRange) |
|
107 | val = (term.text if not isinstance(term, query.DateRange) | |
107 | else [term.startdate, term.enddate]) |
|
108 | else [term.startdate, term.enddate]) | |
@@ -118,7 +119,7 b' def _journal_filter(user_log, search_ter' | |||||
118 | return user_log |
|
119 | return user_log | |
119 |
|
120 | |||
120 |
|
121 | |||
121 | class AdminController(BaseController): |
|
122 | class AdminController(base.BaseController): | |
122 |
|
123 | |||
123 | @LoginRequired(allow_default_user=True) |
|
124 | @LoginRequired(allow_default_user=True) | |
124 | def _before(self, *args, **kwargs): |
|
125 | def _before(self, *args, **kwargs): | |
@@ -126,15 +127,15 b' class AdminController(BaseController):' | |||||
126 |
|
127 | |||
127 | @HasPermissionAnyDecorator('hg.admin') |
|
128 | @HasPermissionAnyDecorator('hg.admin') | |
128 | def index(self): |
|
129 | def index(self): | |
129 | users_log = UserLog.query() \ |
|
130 | users_log = db.UserLog.query() \ | |
130 | .options(joinedload(UserLog.user)) \ |
|
131 | .options(joinedload(db.UserLog.user)) \ | |
131 | .options(joinedload(UserLog.repository)) |
|
132 | .options(joinedload(db.UserLog.repository)) | |
132 |
|
133 | |||
133 | # FILTERING |
|
134 | # FILTERING | |
134 | c.search_term = request.GET.get('filter') |
|
135 | c.search_term = request.GET.get('filter') | |
135 | users_log = _journal_filter(users_log, c.search_term) |
|
136 | users_log = _journal_filter(users_log, c.search_term) | |
136 |
|
137 | |||
137 | users_log = users_log.order_by(UserLog.action_date.desc()) |
|
138 | users_log = users_log.order_by(db.UserLog.action_date.desc()) | |
138 |
|
139 | |||
139 | p = safe_int(request.GET.get('page'), 1) |
|
140 | p = safe_int(request.GET.get('page'), 1) | |
140 |
|
141 | |||
@@ -142,6 +143,6 b' class AdminController(BaseController):' | |||||
142 | filter=c.search_term) |
|
143 | filter=c.search_term) | |
143 |
|
144 | |||
144 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
145 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
145 | return render('admin/admin_log.html') |
|
146 | return base.render('admin/admin_log.html') | |
146 |
|
147 | |||
147 | return render('admin/admin.html') |
|
148 | return base.render('admin/admin.html') |
@@ -32,20 +32,18 b' from tg import tmpl_context as c' | |||||
32 | from tg.i18n import ugettext as _ |
|
32 | from tg.i18n import ugettext as _ | |
33 | from webob.exc import HTTPFound |
|
33 | from webob.exc import HTTPFound | |
34 |
|
34 | |||
35 |
from kallithea.con |
|
35 | from kallithea.controllers import base | |
36 | from kallithea.lib import auth_modules |
|
36 | from kallithea.lib import auth_modules, webutils | |
37 | from kallithea.lib import helpers as h |
|
|||
38 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired |
|
37 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired | |
39 |
from kallithea.lib. |
|
38 | from kallithea.lib.webutils import url | |
40 |
from kallithea.model |
|
39 | from kallithea.model import db, meta | |
41 | from kallithea.model.forms import AuthSettingsForm |
|
40 | from kallithea.model.forms import AuthSettingsForm | |
42 | from kallithea.model.meta import Session |
|
|||
43 |
|
41 | |||
44 |
|
42 | |||
45 | log = logging.getLogger(__name__) |
|
43 | log = logging.getLogger(__name__) | |
46 |
|
44 | |||
47 |
|
45 | |||
48 | class AuthSettingsController(BaseController): |
|
46 | class AuthSettingsController(base.BaseController): | |
49 |
|
47 | |||
50 | @LoginRequired() |
|
48 | @LoginRequired() | |
51 | @HasPermissionAnyDecorator('hg.admin') |
|
49 | @HasPermissionAnyDecorator('hg.admin') | |
@@ -77,7 +75,7 b' class AuthSettingsController(BaseControl' | |||||
77 | if "default" in v: |
|
75 | if "default" in v: | |
78 | c.defaults[fullname] = v["default"] |
|
76 | c.defaults[fullname] = v["default"] | |
79 | # Current values will be the default on the form, if there are any |
|
77 | # Current values will be the default on the form, if there are any | |
80 | setting = Setting.get_by_name(fullname) |
|
78 | setting = db.Setting.get_by_name(fullname) | |
81 | if setting is not None: |
|
79 | if setting is not None: | |
82 | c.defaults[fullname] = setting.app_settings_value |
|
80 | c.defaults[fullname] = setting.app_settings_value | |
83 | if defaults: |
|
81 | if defaults: | |
@@ -88,7 +86,7 b' class AuthSettingsController(BaseControl' | |||||
88 |
|
86 | |||
89 | log.debug('defaults: %s', defaults) |
|
87 | log.debug('defaults: %s', defaults) | |
90 | return formencode.htmlfill.render( |
|
88 | return formencode.htmlfill.render( | |
91 | render('admin/auth/auth_settings.html'), |
|
89 | base.render('admin/auth/auth_settings.html'), | |
92 | defaults=c.defaults, |
|
90 | defaults=c.defaults, | |
93 | errors=errors, |
|
91 | errors=errors, | |
94 | prefix_error=False, |
|
92 | prefix_error=False, | |
@@ -131,9 +129,9 b' class AuthSettingsController(BaseControl' | |||||
131 | # we want to store it comma separated inside our settings |
|
129 | # we want to store it comma separated inside our settings | |
132 | v = ','.join(v) |
|
130 | v = ','.join(v) | |
133 | log.debug("%s = %s", k, str(v)) |
|
131 | log.debug("%s = %s", k, str(v)) | |
134 | setting = Setting.create_or_update(k, v) |
|
132 | setting = db.Setting.create_or_update(k, v) | |
135 | Session().commit() |
|
133 | meta.Session().commit() | |
136 |
|
|
134 | webutils.flash(_('Auth settings updated successfully'), | |
137 | category='success') |
|
135 | category='success') | |
138 | except formencode.Invalid as errors: |
|
136 | except formencode.Invalid as errors: | |
139 | log.error(traceback.format_exc()) |
|
137 | log.error(traceback.format_exc()) | |
@@ -144,7 +142,7 b' class AuthSettingsController(BaseControl' | |||||
144 | ) |
|
142 | ) | |
145 | except Exception: |
|
143 | except Exception: | |
146 | log.error(traceback.format_exc()) |
|
144 | log.error(traceback.format_exc()) | |
147 |
|
|
145 | webutils.flash(_('error occurred during update of auth settings'), | |
148 | category='error') |
|
146 | category='error') | |
149 |
|
147 | |||
150 | raise HTTPFound(location=url('auth_home')) |
|
148 | raise HTTPFound(location=url('auth_home')) |
@@ -34,19 +34,18 b' from tg import request' | |||||
34 | from tg.i18n import ugettext as _ |
|
34 | from tg.i18n import ugettext as _ | |
35 | from webob.exc import HTTPFound |
|
35 | from webob.exc import HTTPFound | |
36 |
|
36 | |||
37 |
from kallithea.con |
|
37 | from kallithea.controllers import base | |
38 |
from kallithea.lib import |
|
38 | from kallithea.lib import webutils | |
39 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired |
|
39 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired | |
40 |
from kallithea.lib. |
|
40 | from kallithea.lib.webutils import url | |
41 |
from kallithea.model |
|
41 | from kallithea.model import db, meta | |
42 | from kallithea.model.forms import DefaultsForm |
|
42 | from kallithea.model.forms import DefaultsForm | |
43 | from kallithea.model.meta import Session |
|
|||
44 |
|
43 | |||
45 |
|
44 | |||
46 | log = logging.getLogger(__name__) |
|
45 | log = logging.getLogger(__name__) | |
47 |
|
46 | |||
48 |
|
47 | |||
49 | class DefaultsController(BaseController): |
|
48 | class DefaultsController(base.BaseController): | |
50 |
|
49 | |||
51 | @LoginRequired() |
|
50 | @LoginRequired() | |
52 | @HasPermissionAnyDecorator('hg.admin') |
|
51 | @HasPermissionAnyDecorator('hg.admin') | |
@@ -54,10 +53,10 b' class DefaultsController(BaseController)' | |||||
54 | super(DefaultsController, self)._before(*args, **kwargs) |
|
53 | super(DefaultsController, self)._before(*args, **kwargs) | |
55 |
|
54 | |||
56 | def index(self, format='html'): |
|
55 | def index(self, format='html'): | |
57 | defaults = Setting.get_default_repo_settings() |
|
56 | defaults = db.Setting.get_default_repo_settings() | |
58 |
|
57 | |||
59 | return htmlfill.render( |
|
58 | return htmlfill.render( | |
60 | render('admin/defaults/defaults.html'), |
|
59 | base.render('admin/defaults/defaults.html'), | |
61 | defaults=defaults, |
|
60 | defaults=defaults, | |
62 | encoding="UTF-8", |
|
61 | encoding="UTF-8", | |
63 | force_defaults=False |
|
62 | force_defaults=False | |
@@ -69,16 +68,16 b' class DefaultsController(BaseController)' | |||||
69 | try: |
|
68 | try: | |
70 | form_result = _form.to_python(dict(request.POST)) |
|
69 | form_result = _form.to_python(dict(request.POST)) | |
71 | for k, v in form_result.items(): |
|
70 | for k, v in form_result.items(): | |
72 | setting = Setting.create_or_update(k, v) |
|
71 | setting = db.Setting.create_or_update(k, v) | |
73 | Session().commit() |
|
72 | meta.Session().commit() | |
74 |
|
|
73 | webutils.flash(_('Default settings updated successfully'), | |
75 | category='success') |
|
74 | category='success') | |
76 |
|
75 | |||
77 | except formencode.Invalid as errors: |
|
76 | except formencode.Invalid as errors: | |
78 | defaults = errors.value |
|
77 | defaults = errors.value | |
79 |
|
78 | |||
80 | return htmlfill.render( |
|
79 | return htmlfill.render( | |
81 | render('admin/defaults/defaults.html'), |
|
80 | base.render('admin/defaults/defaults.html'), | |
82 | defaults=defaults, |
|
81 | defaults=defaults, | |
83 | errors=errors.error_dict or {}, |
|
82 | errors=errors.error_dict or {}, | |
84 | prefix_error=False, |
|
83 | prefix_error=False, | |
@@ -86,7 +85,7 b' class DefaultsController(BaseController)' | |||||
86 | force_defaults=False) |
|
85 | force_defaults=False) | |
87 | except Exception: |
|
86 | except Exception: | |
88 | log.error(traceback.format_exc()) |
|
87 | log.error(traceback.format_exc()) | |
89 |
|
|
88 | webutils.flash(_('Error occurred during update of defaults'), | |
90 | category='error') |
|
89 | category='error') | |
91 |
|
90 | |||
92 | raise HTTPFound(location=url('defaults')) |
|
91 | raise HTTPFound(location=url('defaults')) |
@@ -35,24 +35,22 b' from tg import tmpl_context as c' | |||||
35 | from tg.i18n import ugettext as _ |
|
35 | from tg.i18n import ugettext as _ | |
36 | from webob.exc import HTTPForbidden, HTTPFound, HTTPNotFound |
|
36 | from webob.exc import HTTPForbidden, HTTPFound, HTTPNotFound | |
37 |
|
37 | |||
38 |
from kallithea.con |
|
38 | from kallithea.controllers import base | |
39 |
from kallithea.lib import |
|
39 | from kallithea.lib import auth, webutils | |
40 | from kallithea.lib.auth import LoginRequired |
|
40 | from kallithea.lib.auth import LoginRequired | |
41 | from kallithea.lib.base import BaseController, jsonify, render |
|
|||
42 | from kallithea.lib.page import Page |
|
41 | from kallithea.lib.page import Page | |
43 | from kallithea.lib.utils2 import safe_int, safe_str, time_to_datetime |
|
42 | from kallithea.lib.utils2 import safe_int, safe_str, time_to_datetime | |
44 | from kallithea.lib.vcs.exceptions import NodeNotChangedError, VCSError |
|
43 | from kallithea.lib.vcs.exceptions import NodeNotChangedError, VCSError | |
45 |
from kallithea. |
|
44 | from kallithea.lib.webutils import url | |
|
45 | from kallithea.model import db, meta | |||
46 | from kallithea.model.forms import GistForm |
|
46 | from kallithea.model.forms import GistForm | |
47 | from kallithea.model.gist import GistModel |
|
47 | from kallithea.model.gist import GistModel | |
48 | from kallithea.model.meta import Session |
|
|||
49 |
|
48 | |||
50 |
|
49 | |||
51 | log = logging.getLogger(__name__) |
|
50 | log = logging.getLogger(__name__) | |
52 |
|
51 | |||
53 |
|
52 | |||
54 | class GistsController(BaseController): |
|
53 | class GistsController(base.BaseController): | |
55 | """REST Controller styled on the Atom Publishing Protocol""" |
|
|||
56 |
|
54 | |||
57 | def __load_defaults(self, extra_values=None): |
|
55 | def __load_defaults(self, extra_values=None): | |
58 | c.lifetime_values = [ |
|
56 | c.lifetime_values = [ | |
@@ -77,34 +75,34 b' class GistsController(BaseController):' | |||||
77 | elif c.show_private: |
|
75 | elif c.show_private: | |
78 | url_params['private'] = 1 |
|
76 | url_params['private'] = 1 | |
79 |
|
77 | |||
80 | gists = Gist().query() \ |
|
78 | gists = db.Gist().query() \ | |
81 | .filter_by(is_expired=False) \ |
|
79 | .filter_by(is_expired=False) \ | |
82 | .order_by(Gist.created_on.desc()) |
|
80 | .order_by(db.Gist.created_on.desc()) | |
83 |
|
81 | |||
84 | # MY private |
|
82 | # MY private | |
85 | if c.show_private and not c.show_public: |
|
83 | if c.show_private and not c.show_public: | |
86 | gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE) \ |
|
84 | gists = gists.filter(db.Gist.gist_type == db.Gist.GIST_PRIVATE) \ | |
87 | .filter(Gist.owner_id == request.authuser.user_id) |
|
85 | .filter(db.Gist.owner_id == request.authuser.user_id) | |
88 | # MY public |
|
86 | # MY public | |
89 | elif c.show_public and not c.show_private: |
|
87 | elif c.show_public and not c.show_private: | |
90 | gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) \ |
|
88 | gists = gists.filter(db.Gist.gist_type == db.Gist.GIST_PUBLIC) \ | |
91 | .filter(Gist.owner_id == request.authuser.user_id) |
|
89 | .filter(db.Gist.owner_id == request.authuser.user_id) | |
92 |
|
90 | |||
93 | # MY public+private |
|
91 | # MY public+private | |
94 | elif c.show_private and c.show_public: |
|
92 | elif c.show_private and c.show_public: | |
95 | gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC, |
|
93 | gists = gists.filter(or_(db.Gist.gist_type == db.Gist.GIST_PUBLIC, | |
96 | Gist.gist_type == Gist.GIST_PRIVATE)) \ |
|
94 | db.Gist.gist_type == db.Gist.GIST_PRIVATE)) \ | |
97 | .filter(Gist.owner_id == request.authuser.user_id) |
|
95 | .filter(db.Gist.owner_id == request.authuser.user_id) | |
98 |
|
96 | |||
99 | # default show ALL public gists |
|
97 | # default show ALL public gists | |
100 | if not c.show_public and not c.show_private: |
|
98 | if not c.show_public and not c.show_private: | |
101 | gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) |
|
99 | gists = gists.filter(db.Gist.gist_type == db.Gist.GIST_PUBLIC) | |
102 |
|
100 | |||
103 | c.gists = gists |
|
101 | c.gists = gists | |
104 | p = safe_int(request.GET.get('page'), 1) |
|
102 | p = safe_int(request.GET.get('page'), 1) | |
105 | c.gists_pager = Page(c.gists, page=p, items_per_page=10, |
|
103 | c.gists_pager = Page(c.gists, page=p, items_per_page=10, | |
106 | **url_params) |
|
104 | **url_params) | |
107 | return render('admin/gists/index.html') |
|
105 | return base.render('admin/gists/index.html') | |
108 |
|
106 | |||
109 | @LoginRequired() |
|
107 | @LoginRequired() | |
110 | def create(self): |
|
108 | def create(self): | |
@@ -113,7 +111,7 b' class GistsController(BaseController):' | |||||
113 | try: |
|
111 | try: | |
114 | form_result = gist_form.to_python(dict(request.POST)) |
|
112 | form_result = gist_form.to_python(dict(request.POST)) | |
115 | # TODO: multiple files support, from the form |
|
113 | # TODO: multiple files support, from the form | |
116 | filename = form_result['filename'] or Gist.DEFAULT_FILENAME |
|
114 | filename = form_result['filename'] or db.Gist.DEFAULT_FILENAME | |
117 | nodes = { |
|
115 | nodes = { | |
118 | filename: { |
|
116 | filename: { | |
119 | 'content': form_result['content'], |
|
117 | 'content': form_result['content'], | |
@@ -121,7 +119,7 b' class GistsController(BaseController):' | |||||
121 | } |
|
119 | } | |
122 | } |
|
120 | } | |
123 | _public = form_result['public'] |
|
121 | _public = form_result['public'] | |
124 | gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE |
|
122 | gist_type = db.Gist.GIST_PUBLIC if _public else db.Gist.GIST_PRIVATE | |
125 | gist = GistModel().create( |
|
123 | gist = GistModel().create( | |
126 | description=form_result['description'], |
|
124 | description=form_result['description'], | |
127 | owner=request.authuser.user_id, |
|
125 | owner=request.authuser.user_id, | |
@@ -130,13 +128,13 b' class GistsController(BaseController):' | |||||
130 | gist_type=gist_type, |
|
128 | gist_type=gist_type, | |
131 | lifetime=form_result['lifetime'] |
|
129 | lifetime=form_result['lifetime'] | |
132 | ) |
|
130 | ) | |
133 | Session().commit() |
|
131 | meta.Session().commit() | |
134 | new_gist_id = gist.gist_access_id |
|
132 | new_gist_id = gist.gist_access_id | |
135 | except formencode.Invalid as errors: |
|
133 | except formencode.Invalid as errors: | |
136 | defaults = errors.value |
|
134 | defaults = errors.value | |
137 |
|
135 | |||
138 | return formencode.htmlfill.render( |
|
136 | return formencode.htmlfill.render( | |
139 | render('admin/gists/new.html'), |
|
137 | base.render('admin/gists/new.html'), | |
140 | defaults=defaults, |
|
138 | defaults=defaults, | |
141 | errors=errors.error_dict or {}, |
|
139 | errors=errors.error_dict or {}, | |
142 | prefix_error=False, |
|
140 | prefix_error=False, | |
@@ -145,23 +143,23 b' class GistsController(BaseController):' | |||||
145 |
|
143 | |||
146 | except Exception as e: |
|
144 | except Exception as e: | |
147 | log.error(traceback.format_exc()) |
|
145 | log.error(traceback.format_exc()) | |
148 |
|
|
146 | webutils.flash(_('Error occurred during gist creation'), category='error') | |
149 | raise HTTPFound(location=url('new_gist')) |
|
147 | raise HTTPFound(location=url('new_gist')) | |
150 | raise HTTPFound(location=url('gist', gist_id=new_gist_id)) |
|
148 | raise HTTPFound(location=url('gist', gist_id=new_gist_id)) | |
151 |
|
149 | |||
152 | @LoginRequired() |
|
150 | @LoginRequired() | |
153 | def new(self, format='html'): |
|
151 | def new(self, format='html'): | |
154 | self.__load_defaults() |
|
152 | self.__load_defaults() | |
155 | return render('admin/gists/new.html') |
|
153 | return base.render('admin/gists/new.html') | |
156 |
|
154 | |||
157 | @LoginRequired() |
|
155 | @LoginRequired() | |
158 | def delete(self, gist_id): |
|
156 | def delete(self, gist_id): | |
159 | gist = GistModel().get_gist(gist_id) |
|
157 | gist = GistModel().get_gist(gist_id) | |
160 | owner = gist.owner_id == request.authuser.user_id |
|
158 | owner = gist.owner_id == request.authuser.user_id | |
161 | if h.HasPermissionAny('hg.admin')() or owner: |
|
159 | if auth.HasPermissionAny('hg.admin')() or owner: | |
162 | GistModel().delete(gist) |
|
160 | GistModel().delete(gist) | |
163 | Session().commit() |
|
161 | meta.Session().commit() | |
164 |
|
|
162 | webutils.flash(_('Deleted gist %s') % gist.gist_access_id, category='success') | |
165 | else: |
|
163 | else: | |
166 | raise HTTPForbidden() |
|
164 | raise HTTPForbidden() | |
167 |
|
165 | |||
@@ -169,7 +167,7 b' class GistsController(BaseController):' | |||||
169 |
|
167 | |||
170 | @LoginRequired(allow_default_user=True) |
|
168 | @LoginRequired(allow_default_user=True) | |
171 | def show(self, gist_id, revision='tip', format='html', f_path=None): |
|
169 | def show(self, gist_id, revision='tip', format='html', f_path=None): | |
172 | c.gist = Gist.get_or_404(gist_id) |
|
170 | c.gist = db.Gist.get_or_404(gist_id) | |
173 |
|
171 | |||
174 | if c.gist.is_expired: |
|
172 | if c.gist.is_expired: | |
175 | log.error('Gist expired at %s', |
|
173 | log.error('Gist expired at %s', | |
@@ -188,11 +186,11 b' class GistsController(BaseController):' | |||||
188 | ) |
|
186 | ) | |
189 | response.content_type = 'text/plain' |
|
187 | response.content_type = 'text/plain' | |
190 | return content |
|
188 | return content | |
191 | return render('admin/gists/show.html') |
|
189 | return base.render('admin/gists/show.html') | |
192 |
|
190 | |||
193 | @LoginRequired() |
|
191 | @LoginRequired() | |
194 | def edit(self, gist_id, format='html'): |
|
192 | def edit(self, gist_id, format='html'): | |
195 | c.gist = Gist.get_or_404(gist_id) |
|
193 | c.gist = db.Gist.get_or_404(gist_id) | |
196 |
|
194 | |||
197 | if c.gist.is_expired: |
|
195 | if c.gist.is_expired: | |
198 | log.error('Gist expired at %s', |
|
196 | log.error('Gist expired at %s', | |
@@ -205,7 +203,7 b' class GistsController(BaseController):' | |||||
205 | raise HTTPNotFound() |
|
203 | raise HTTPNotFound() | |
206 |
|
204 | |||
207 | self.__load_defaults(extra_values=('0', _('Unmodified'))) |
|
205 | self.__load_defaults(extra_values=('0', _('Unmodified'))) | |
208 | rendered = render('admin/gists/edit.html') |
|
206 | rendered = base.render('admin/gists/edit.html') | |
209 |
|
207 | |||
210 | if request.POST: |
|
208 | if request.POST: | |
211 | rpost = request.POST |
|
209 | rpost = request.POST | |
@@ -233,16 +231,16 b' class GistsController(BaseController):' | |||||
233 | lifetime=rpost['lifetime'] |
|
231 | lifetime=rpost['lifetime'] | |
234 | ) |
|
232 | ) | |
235 |
|
233 | |||
236 | Session().commit() |
|
234 | meta.Session().commit() | |
237 |
|
|
235 | webutils.flash(_('Successfully updated gist content'), category='success') | |
238 | except NodeNotChangedError: |
|
236 | except NodeNotChangedError: | |
239 | # raised if nothing was changed in repo itself. We anyway then |
|
237 | # raised if nothing was changed in repo itself. We anyway then | |
240 | # store only DB stuff for gist |
|
238 | # store only DB stuff for gist | |
241 | Session().commit() |
|
239 | meta.Session().commit() | |
242 |
|
|
240 | webutils.flash(_('Successfully updated gist data'), category='success') | |
243 | except Exception: |
|
241 | except Exception: | |
244 | log.error(traceback.format_exc()) |
|
242 | log.error(traceback.format_exc()) | |
245 |
|
|
243 | webutils.flash(_('Error occurred during update of gist %s') % gist_id, | |
246 | category='error') |
|
244 | category='error') | |
247 |
|
245 | |||
248 | raise HTTPFound(location=url('gist', gist_id=gist_id)) |
|
246 | raise HTTPFound(location=url('gist', gist_id=gist_id)) | |
@@ -250,9 +248,9 b' class GistsController(BaseController):' | |||||
250 | return rendered |
|
248 | return rendered | |
251 |
|
249 | |||
252 | @LoginRequired() |
|
250 | @LoginRequired() | |
253 | @jsonify |
|
251 | @base.jsonify | |
254 | def check_revision(self, gist_id): |
|
252 | def check_revision(self, gist_id): | |
255 | c.gist = Gist.get_or_404(gist_id) |
|
253 | c.gist = db.Gist.get_or_404(gist_id) | |
256 | last_rev = c.gist.scm_instance.get_changeset() |
|
254 | last_rev = c.gist.scm_instance.get_changeset() | |
257 | success = True |
|
255 | success = True | |
258 | revision = request.POST.get('revision') |
|
256 | revision = request.POST.get('revision') |
@@ -35,16 +35,14 b' from tg import tmpl_context as c' | |||||
35 | from tg.i18n import ugettext as _ |
|
35 | from tg.i18n import ugettext as _ | |
36 | from webob.exc import HTTPFound |
|
36 | from webob.exc import HTTPFound | |
37 |
|
37 | |||
38 |
from kallithea.con |
|
38 | from kallithea.controllers import base | |
39 | from kallithea.lib import auth_modules |
|
39 | from kallithea.lib import auth_modules, webutils | |
40 | from kallithea.lib import helpers as h |
|
|||
41 | from kallithea.lib.auth import AuthUser, LoginRequired |
|
40 | from kallithea.lib.auth import AuthUser, LoginRequired | |
42 | from kallithea.lib.base import BaseController, IfSshEnabled, render |
|
|||
43 | from kallithea.lib.utils2 import generate_api_key, safe_int |
|
41 | from kallithea.lib.utils2 import generate_api_key, safe_int | |
|
42 | from kallithea.lib.webutils import url | |||
|
43 | from kallithea.model import db, meta | |||
44 | from kallithea.model.api_key import ApiKeyModel |
|
44 | from kallithea.model.api_key import ApiKeyModel | |
45 | from kallithea.model.db import Repository, User, UserEmailMap, UserFollowing |
|
|||
46 | from kallithea.model.forms import PasswordChangeForm, UserForm |
|
45 | from kallithea.model.forms import PasswordChangeForm, UserForm | |
47 | from kallithea.model.meta import Session |
|
|||
48 | from kallithea.model.repo import RepoModel |
|
46 | from kallithea.model.repo import RepoModel | |
49 | from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException |
|
47 | from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException | |
50 | from kallithea.model.user import UserModel |
|
48 | from kallithea.model.user import UserModel | |
@@ -53,35 +51,30 b' from kallithea.model.user import UserMod' | |||||
53 | log = logging.getLogger(__name__) |
|
51 | log = logging.getLogger(__name__) | |
54 |
|
52 | |||
55 |
|
53 | |||
56 | class MyAccountController(BaseController): |
|
54 | class MyAccountController(base.BaseController): | |
57 | """REST Controller styled on the Atom Publishing Protocol""" |
|
|||
58 | # To properly map this controller, ensure your config/routing.py |
|
|||
59 | # file has a resource setup: |
|
|||
60 | # map.resource('setting', 'settings', controller='admin/settings', |
|
|||
61 | # path_prefix='/admin', name_prefix='admin_') |
|
|||
62 |
|
55 | |||
63 | @LoginRequired() |
|
56 | @LoginRequired() | |
64 | def _before(self, *args, **kwargs): |
|
57 | def _before(self, *args, **kwargs): | |
65 | super(MyAccountController, self)._before(*args, **kwargs) |
|
58 | super(MyAccountController, self)._before(*args, **kwargs) | |
66 |
|
59 | |||
67 | def __load_data(self): |
|
60 | def __load_data(self): | |
68 | c.user = User.get(request.authuser.user_id) |
|
61 | c.user = db.User.get(request.authuser.user_id) | |
69 | if c.user.is_default_user: |
|
62 | if c.user.is_default_user: | |
70 |
|
|
63 | webutils.flash(_("You can't edit this user since it's" | |
71 | " crucial for entire application"), category='warning') |
|
64 | " crucial for entire application"), category='warning') | |
72 | raise HTTPFound(location=url('users')) |
|
65 | raise HTTPFound(location=url('users')) | |
73 |
|
66 | |||
74 | def _load_my_repos_data(self, watched=False): |
|
67 | def _load_my_repos_data(self, watched=False): | |
75 | if watched: |
|
68 | if watched: | |
76 | admin = False |
|
69 | admin = False | |
77 | repos_list = Session().query(Repository) \ |
|
70 | repos_list = meta.Session().query(db.Repository) \ | |
78 | .join(UserFollowing) \ |
|
71 | .join(db.UserFollowing) \ | |
79 | .filter(UserFollowing.user_id == |
|
72 | .filter(db.UserFollowing.user_id == | |
80 | request.authuser.user_id).all() |
|
73 | request.authuser.user_id).all() | |
81 | else: |
|
74 | else: | |
82 | admin = True |
|
75 | admin = True | |
83 | repos_list = Session().query(Repository) \ |
|
76 | repos_list = meta.Session().query(db.Repository) \ | |
84 | .filter(Repository.owner_id == |
|
77 | .filter(db.Repository.owner_id == | |
85 | request.authuser.user_id).all() |
|
78 | request.authuser.user_id).all() | |
86 |
|
79 | |||
87 | return RepoModel().get_repos_as_dict(repos_list, admin=admin) |
|
80 | return RepoModel().get_repos_as_dict(repos_list, admin=admin) | |
@@ -91,7 +84,7 b' class MyAccountController(BaseController' | |||||
91 | self.__load_data() |
|
84 | self.__load_data() | |
92 | c.perm_user = AuthUser(user_id=request.authuser.user_id) |
|
85 | c.perm_user = AuthUser(user_id=request.authuser.user_id) | |
93 | managed_fields = auth_modules.get_managed_fields(c.user) |
|
86 | managed_fields = auth_modules.get_managed_fields(c.user) | |
94 |
def_user_perms = AuthUser(dbuser=User.get_default_user()).permissions |
|
87 | def_user_perms = AuthUser(dbuser=db.User.get_default_user()).global_permissions | |
95 | if 'hg.register.none' in def_user_perms: |
|
88 | if 'hg.register.none' in def_user_perms: | |
96 | managed_fields.extend(['username', 'firstname', 'lastname', 'email']) |
|
89 | managed_fields.extend(['username', 'firstname', 'lastname', 'email']) | |
97 |
|
90 | |||
@@ -116,14 +109,14 b' class MyAccountController(BaseController' | |||||
116 |
|
109 | |||
117 | UserModel().update(request.authuser.user_id, form_result, |
|
110 | UserModel().update(request.authuser.user_id, form_result, | |
118 | skip_attrs=skip_attrs) |
|
111 | skip_attrs=skip_attrs) | |
119 |
|
|
112 | webutils.flash(_('Your account was updated successfully'), | |
120 | category='success') |
|
113 | category='success') | |
121 | Session().commit() |
|
114 | meta.Session().commit() | |
122 | update = True |
|
115 | update = True | |
123 |
|
116 | |||
124 | except formencode.Invalid as errors: |
|
117 | except formencode.Invalid as errors: | |
125 | return htmlfill.render( |
|
118 | return htmlfill.render( | |
126 | render('admin/my_account/my_account.html'), |
|
119 | base.render('admin/my_account/my_account.html'), | |
127 | defaults=errors.value, |
|
120 | defaults=errors.value, | |
128 | errors=errors.error_dict or {}, |
|
121 | errors=errors.error_dict or {}, | |
129 | prefix_error=False, |
|
122 | prefix_error=False, | |
@@ -131,12 +124,12 b' class MyAccountController(BaseController' | |||||
131 | force_defaults=False) |
|
124 | force_defaults=False) | |
132 | except Exception: |
|
125 | except Exception: | |
133 | log.error(traceback.format_exc()) |
|
126 | log.error(traceback.format_exc()) | |
134 |
|
|
127 | webutils.flash(_('Error occurred during update of user %s') | |
135 | % form_result.get('username'), category='error') |
|
128 | % form_result.get('username'), category='error') | |
136 | if update: |
|
129 | if update: | |
137 | raise HTTPFound(location='my_account') |
|
130 | raise HTTPFound(location='my_account') | |
138 | return htmlfill.render( |
|
131 | return htmlfill.render( | |
139 | render('admin/my_account/my_account.html'), |
|
132 | base.render('admin/my_account/my_account.html'), | |
140 | defaults=defaults, |
|
133 | defaults=defaults, | |
141 | encoding="UTF-8", |
|
134 | encoding="UTF-8", | |
142 | force_defaults=False) |
|
135 | force_defaults=False) | |
@@ -153,11 +146,11 b' class MyAccountController(BaseController' | |||||
153 | try: |
|
146 | try: | |
154 | form_result = _form.to_python(request.POST) |
|
147 | form_result = _form.to_python(request.POST) | |
155 | UserModel().update(request.authuser.user_id, form_result) |
|
148 | UserModel().update(request.authuser.user_id, form_result) | |
156 | Session().commit() |
|
149 | meta.Session().commit() | |
157 |
|
|
150 | webutils.flash(_("Successfully updated password"), category='success') | |
158 | except formencode.Invalid as errors: |
|
151 | except formencode.Invalid as errors: | |
159 | return htmlfill.render( |
|
152 | return htmlfill.render( | |
160 | render('admin/my_account/my_account.html'), |
|
153 | base.render('admin/my_account/my_account.html'), | |
161 | defaults=errors.value, |
|
154 | defaults=errors.value, | |
162 | errors=errors.error_dict or {}, |
|
155 | errors=errors.error_dict or {}, | |
163 | prefix_error=False, |
|
156 | prefix_error=False, | |
@@ -165,9 +158,9 b' class MyAccountController(BaseController' | |||||
165 | force_defaults=False) |
|
158 | force_defaults=False) | |
166 | except Exception: |
|
159 | except Exception: | |
167 | log.error(traceback.format_exc()) |
|
160 | log.error(traceback.format_exc()) | |
168 |
|
|
161 | webutils.flash(_('Error occurred during update of user password'), | |
169 | category='error') |
|
162 | category='error') | |
170 | return render('admin/my_account/my_account.html') |
|
163 | return base.render('admin/my_account/my_account.html') | |
171 |
|
164 | |||
172 | def my_account_repos(self): |
|
165 | def my_account_repos(self): | |
173 | c.active = 'repos' |
|
166 | c.active = 'repos' | |
@@ -175,7 +168,7 b' class MyAccountController(BaseController' | |||||
175 |
|
168 | |||
176 | # data used to render the grid |
|
169 | # data used to render the grid | |
177 | c.data = self._load_my_repos_data() |
|
170 | c.data = self._load_my_repos_data() | |
178 | return render('admin/my_account/my_account.html') |
|
171 | return base.render('admin/my_account/my_account.html') | |
179 |
|
172 | |||
180 | def my_account_watched(self): |
|
173 | def my_account_watched(self): | |
181 | c.active = 'watched' |
|
174 | c.active = 'watched' | |
@@ -183,36 +176,36 b' class MyAccountController(BaseController' | |||||
183 |
|
176 | |||
184 | # data used to render the grid |
|
177 | # data used to render the grid | |
185 | c.data = self._load_my_repos_data(watched=True) |
|
178 | c.data = self._load_my_repos_data(watched=True) | |
186 | return render('admin/my_account/my_account.html') |
|
179 | return base.render('admin/my_account/my_account.html') | |
187 |
|
180 | |||
188 | def my_account_perms(self): |
|
181 | def my_account_perms(self): | |
189 | c.active = 'perms' |
|
182 | c.active = 'perms' | |
190 | self.__load_data() |
|
183 | self.__load_data() | |
191 | c.perm_user = AuthUser(user_id=request.authuser.user_id) |
|
184 | c.perm_user = AuthUser(user_id=request.authuser.user_id) | |
192 |
|
185 | |||
193 | return render('admin/my_account/my_account.html') |
|
186 | return base.render('admin/my_account/my_account.html') | |
194 |
|
187 | |||
195 | def my_account_emails(self): |
|
188 | def my_account_emails(self): | |
196 | c.active = 'emails' |
|
189 | c.active = 'emails' | |
197 | self.__load_data() |
|
190 | self.__load_data() | |
198 |
|
191 | |||
199 | c.user_email_map = UserEmailMap.query() \ |
|
192 | c.user_email_map = db.UserEmailMap.query() \ | |
200 | .filter(UserEmailMap.user == c.user).all() |
|
193 | .filter(db.UserEmailMap.user == c.user).all() | |
201 | return render('admin/my_account/my_account.html') |
|
194 | return base.render('admin/my_account/my_account.html') | |
202 |
|
195 | |||
203 | def my_account_emails_add(self): |
|
196 | def my_account_emails_add(self): | |
204 | email = request.POST.get('new_email') |
|
197 | email = request.POST.get('new_email') | |
205 |
|
198 | |||
206 | try: |
|
199 | try: | |
207 | UserModel().add_extra_email(request.authuser.user_id, email) |
|
200 | UserModel().add_extra_email(request.authuser.user_id, email) | |
208 | Session().commit() |
|
201 | meta.Session().commit() | |
209 |
|
|
202 | webutils.flash(_("Added email %s to user") % email, category='success') | |
210 | except formencode.Invalid as error: |
|
203 | except formencode.Invalid as error: | |
211 | msg = error.error_dict['email'] |
|
204 | msg = error.error_dict['email'] | |
212 |
|
|
205 | webutils.flash(msg, category='error') | |
213 | except Exception: |
|
206 | except Exception: | |
214 | log.error(traceback.format_exc()) |
|
207 | log.error(traceback.format_exc()) | |
215 |
|
|
208 | webutils.flash(_('An error occurred during email saving'), | |
216 | category='error') |
|
209 | category='error') | |
217 | raise HTTPFound(location=url('my_account_emails')) |
|
210 | raise HTTPFound(location=url('my_account_emails')) | |
218 |
|
211 | |||
@@ -220,8 +213,8 b' class MyAccountController(BaseController' | |||||
220 | email_id = request.POST.get('del_email_id') |
|
213 | email_id = request.POST.get('del_email_id') | |
221 | user_model = UserModel() |
|
214 | user_model = UserModel() | |
222 | user_model.delete_extra_email(request.authuser.user_id, email_id) |
|
215 | user_model.delete_extra_email(request.authuser.user_id, email_id) | |
223 | Session().commit() |
|
216 | meta.Session().commit() | |
224 |
|
|
217 | webutils.flash(_("Removed email from user"), category='success') | |
225 | raise HTTPFound(location=url('my_account_emails')) |
|
218 | raise HTTPFound(location=url('my_account_emails')) | |
226 |
|
219 | |||
227 | def my_account_api_keys(self): |
|
220 | def my_account_api_keys(self): | |
@@ -238,59 +231,59 b' class MyAccountController(BaseController' | |||||
238 | c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] |
|
231 | c.lifetime_options = [(c.lifetime_values, _("Lifetime"))] | |
239 | c.user_api_keys = ApiKeyModel().get_api_keys(request.authuser.user_id, |
|
232 | c.user_api_keys = ApiKeyModel().get_api_keys(request.authuser.user_id, | |
240 | show_expired=show_expired) |
|
233 | show_expired=show_expired) | |
241 | return render('admin/my_account/my_account.html') |
|
234 | return base.render('admin/my_account/my_account.html') | |
242 |
|
235 | |||
243 | def my_account_api_keys_add(self): |
|
236 | def my_account_api_keys_add(self): | |
244 | lifetime = safe_int(request.POST.get('lifetime'), -1) |
|
237 | lifetime = safe_int(request.POST.get('lifetime'), -1) | |
245 | description = request.POST.get('description') |
|
238 | description = request.POST.get('description') | |
246 | ApiKeyModel().create(request.authuser.user_id, description, lifetime) |
|
239 | ApiKeyModel().create(request.authuser.user_id, description, lifetime) | |
247 | Session().commit() |
|
240 | meta.Session().commit() | |
248 |
|
|
241 | webutils.flash(_("API key successfully created"), category='success') | |
249 | raise HTTPFound(location=url('my_account_api_keys')) |
|
242 | raise HTTPFound(location=url('my_account_api_keys')) | |
250 |
|
243 | |||
251 | def my_account_api_keys_delete(self): |
|
244 | def my_account_api_keys_delete(self): | |
252 | api_key = request.POST.get('del_api_key') |
|
245 | api_key = request.POST.get('del_api_key') | |
253 | if request.POST.get('del_api_key_builtin'): |
|
246 | if request.POST.get('del_api_key_builtin'): | |
254 | user = User.get(request.authuser.user_id) |
|
247 | user = db.User.get(request.authuser.user_id) | |
255 | user.api_key = generate_api_key() |
|
248 | user.api_key = generate_api_key() | |
256 | Session().commit() |
|
249 | meta.Session().commit() | |
257 |
|
|
250 | webutils.flash(_("API key successfully reset"), category='success') | |
258 | elif api_key: |
|
251 | elif api_key: | |
259 | ApiKeyModel().delete(api_key, request.authuser.user_id) |
|
252 | ApiKeyModel().delete(api_key, request.authuser.user_id) | |
260 | Session().commit() |
|
253 | meta.Session().commit() | |
261 |
|
|
254 | webutils.flash(_("API key successfully deleted"), category='success') | |
262 |
|
255 | |||
263 | raise HTTPFound(location=url('my_account_api_keys')) |
|
256 | raise HTTPFound(location=url('my_account_api_keys')) | |
264 |
|
257 | |||
265 | @IfSshEnabled |
|
258 | @base.IfSshEnabled | |
266 | def my_account_ssh_keys(self): |
|
259 | def my_account_ssh_keys(self): | |
267 | c.active = 'ssh_keys' |
|
260 | c.active = 'ssh_keys' | |
268 | self.__load_data() |
|
261 | self.__load_data() | |
269 | c.user_ssh_keys = SshKeyModel().get_ssh_keys(request.authuser.user_id) |
|
262 | c.user_ssh_keys = SshKeyModel().get_ssh_keys(request.authuser.user_id) | |
270 | return render('admin/my_account/my_account.html') |
|
263 | return base.render('admin/my_account/my_account.html') | |
271 |
|
264 | |||
272 | @IfSshEnabled |
|
265 | @base.IfSshEnabled | |
273 | def my_account_ssh_keys_add(self): |
|
266 | def my_account_ssh_keys_add(self): | |
274 | description = request.POST.get('description') |
|
267 | description = request.POST.get('description') | |
275 | public_key = request.POST.get('public_key') |
|
268 | public_key = request.POST.get('public_key') | |
276 | try: |
|
269 | try: | |
277 | new_ssh_key = SshKeyModel().create(request.authuser.user_id, |
|
270 | new_ssh_key = SshKeyModel().create(request.authuser.user_id, | |
278 | description, public_key) |
|
271 | description, public_key) | |
279 | Session().commit() |
|
272 | meta.Session().commit() | |
280 | SshKeyModel().write_authorized_keys() |
|
273 | SshKeyModel().write_authorized_keys() | |
281 |
|
|
274 | webutils.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success') | |
282 | except SshKeyModelException as e: |
|
275 | except SshKeyModelException as e: | |
283 |
|
|
276 | webutils.flash(e.args[0], category='error') | |
284 | raise HTTPFound(location=url('my_account_ssh_keys')) |
|
277 | raise HTTPFound(location=url('my_account_ssh_keys')) | |
285 |
|
278 | |||
286 | @IfSshEnabled |
|
279 | @base.IfSshEnabled | |
287 | def my_account_ssh_keys_delete(self): |
|
280 | def my_account_ssh_keys_delete(self): | |
288 | fingerprint = request.POST.get('del_public_key_fingerprint') |
|
281 | fingerprint = request.POST.get('del_public_key_fingerprint') | |
289 | try: |
|
282 | try: | |
290 | SshKeyModel().delete(fingerprint, request.authuser.user_id) |
|
283 | SshKeyModel().delete(fingerprint, request.authuser.user_id) | |
291 | Session().commit() |
|
284 | meta.Session().commit() | |
292 | SshKeyModel().write_authorized_keys() |
|
285 | SshKeyModel().write_authorized_keys() | |
293 |
|
|
286 | webutils.flash(_("SSH key successfully deleted"), category='success') | |
294 | except SshKeyModelException as e: |
|
287 | except SshKeyModelException as e: | |
295 |
|
|
288 | webutils.flash(e.args[0], category='error') | |
296 | raise HTTPFound(location=url('my_account_ssh_keys')) |
|
289 | raise HTTPFound(location=url('my_account_ssh_keys')) |
@@ -36,24 +36,19 b' from tg import tmpl_context as c' | |||||
36 | from tg.i18n import ugettext as _ |
|
36 | from tg.i18n import ugettext as _ | |
37 | from webob.exc import HTTPFound |
|
37 | from webob.exc import HTTPFound | |
38 |
|
38 | |||
39 |
from kallithea.con |
|
39 | from kallithea.controllers import base | |
40 |
from kallithea.lib import |
|
40 | from kallithea.lib import webutils | |
41 | from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired |
|
41 | from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired | |
42 |
from kallithea.lib. |
|
42 | from kallithea.lib.webutils import url | |
43 |
from kallithea.model |
|
43 | from kallithea.model import db, meta | |
44 | from kallithea.model.forms import DefaultPermissionsForm |
|
44 | from kallithea.model.forms import DefaultPermissionsForm | |
45 | from kallithea.model.meta import Session |
|
|||
46 | from kallithea.model.permission import PermissionModel |
|
45 | from kallithea.model.permission import PermissionModel | |
47 |
|
46 | |||
48 |
|
47 | |||
49 | log = logging.getLogger(__name__) |
|
48 | log = logging.getLogger(__name__) | |
50 |
|
49 | |||
51 |
|
50 | |||
52 | class PermissionsController(BaseController): |
|
51 | class PermissionsController(base.BaseController): | |
53 | """REST Controller styled on the Atom Publishing Protocol""" |
|
|||
54 | # To properly map this controller, ensure your config/routing.py |
|
|||
55 | # file has a resource setup: |
|
|||
56 | # map.resource('permission', 'permissions') |
|
|||
57 |
|
52 | |||
58 | @LoginRequired() |
|
53 | @LoginRequired() | |
59 | @HasPermissionAnyDecorator('hg.admin') |
|
54 | @HasPermissionAnyDecorator('hg.admin') | |
@@ -61,18 +56,22 b' class PermissionsController(BaseControll' | |||||
61 | super(PermissionsController, self)._before(*args, **kwargs) |
|
56 | super(PermissionsController, self)._before(*args, **kwargs) | |
62 |
|
57 | |||
63 | def __load_data(self): |
|
58 | def __load_data(self): | |
|
59 | # Permissions for the Default user on new repositories | |||
64 | c.repo_perms_choices = [('repository.none', _('None'),), |
|
60 | c.repo_perms_choices = [('repository.none', _('None'),), | |
65 | ('repository.read', _('Read'),), |
|
61 | ('repository.read', _('Read'),), | |
66 | ('repository.write', _('Write'),), |
|
62 | ('repository.write', _('Write'),), | |
67 | ('repository.admin', _('Admin'),)] |
|
63 | ('repository.admin', _('Admin'),)] | |
|
64 | # Permissions for the Default user on new repository groups | |||
68 | c.group_perms_choices = [('group.none', _('None'),), |
|
65 | c.group_perms_choices = [('group.none', _('None'),), | |
69 | ('group.read', _('Read'),), |
|
66 | ('group.read', _('Read'),), | |
70 | ('group.write', _('Write'),), |
|
67 | ('group.write', _('Write'),), | |
71 | ('group.admin', _('Admin'),)] |
|
68 | ('group.admin', _('Admin'),)] | |
|
69 | # Permissions for the Default user on new user groups | |||
72 | c.user_group_perms_choices = [('usergroup.none', _('None'),), |
|
70 | c.user_group_perms_choices = [('usergroup.none', _('None'),), | |
73 | ('usergroup.read', _('Read'),), |
|
71 | ('usergroup.read', _('Read'),), | |
74 | ('usergroup.write', _('Write'),), |
|
72 | ('usergroup.write', _('Write'),), | |
75 | ('usergroup.admin', _('Admin'),)] |
|
73 | ('usergroup.admin', _('Admin'),)] | |
|
74 | # Registration - allow new Users to create an account | |||
76 | c.register_choices = [ |
|
75 | c.register_choices = [ | |
77 | ('hg.register.none', |
|
76 | ('hg.register.none', | |
78 | _('Disabled')), |
|
77 | _('Disabled')), | |
@@ -80,26 +79,18 b' class PermissionsController(BaseControll' | |||||
80 | _('Allowed with manual account activation')), |
|
79 | _('Allowed with manual account activation')), | |
81 | ('hg.register.auto_activate', |
|
80 | ('hg.register.auto_activate', | |
82 | _('Allowed with automatic account activation')), ] |
|
81 | _('Allowed with automatic account activation')), ] | |
83 |
|
82 | # External auth account activation | ||
84 | c.extern_activate_choices = [ |
|
83 | c.extern_activate_choices = [ | |
85 | ('hg.extern_activate.manual', _('Manual activation of external account')), |
|
84 | ('hg.extern_activate.manual', _('Manual activation of external account')), | |
86 | ('hg.extern_activate.auto', _('Automatic activation of external account')), |
|
85 | ('hg.extern_activate.auto', _('Automatic activation of external account')), | |
87 | ] |
|
86 | ] | |
88 |
|
87 | # Top level repository creation | ||
89 | c.repo_create_choices = [('hg.create.none', _('Disabled')), |
|
88 | c.repo_create_choices = [('hg.create.none', _('Disabled')), | |
90 | ('hg.create.repository', _('Enabled'))] |
|
89 | ('hg.create.repository', _('Enabled'))] | |
91 |
|
90 | # User group creation | ||
92 | c.repo_create_on_write_choices = [ |
|
|||
93 | ('hg.create.write_on_repogroup.true', _('Enabled')), |
|
|||
94 | ('hg.create.write_on_repogroup.false', _('Disabled')), |
|
|||
95 | ] |
|
|||
96 |
|
||||
97 | c.user_group_create_choices = [('hg.usergroup.create.false', _('Disabled')), |
|
91 | c.user_group_create_choices = [('hg.usergroup.create.false', _('Disabled')), | |
98 | ('hg.usergroup.create.true', _('Enabled'))] |
|
92 | ('hg.usergroup.create.true', _('Enabled'))] | |
99 |
|
93 | # Repository forking: | ||
100 | c.repo_group_create_choices = [('hg.repogroup.create.false', _('Disabled')), |
|
|||
101 | ('hg.repogroup.create.true', _('Enabled'))] |
|
|||
102 |
|
||||
103 | c.fork_choices = [('hg.fork.none', _('Disabled')), |
|
94 | c.fork_choices = [('hg.fork.none', _('Disabled')), | |
104 | ('hg.fork.repository', _('Enabled'))] |
|
95 | ('hg.fork.repository', _('Enabled'))] | |
105 |
|
96 | |||
@@ -112,8 +103,6 b' class PermissionsController(BaseControll' | |||||
112 | [x[0] for x in c.group_perms_choices], |
|
103 | [x[0] for x in c.group_perms_choices], | |
113 | [x[0] for x in c.user_group_perms_choices], |
|
104 | [x[0] for x in c.user_group_perms_choices], | |
114 | [x[0] for x in c.repo_create_choices], |
|
105 | [x[0] for x in c.repo_create_choices], | |
115 | [x[0] for x in c.repo_create_on_write_choices], |
|
|||
116 | [x[0] for x in c.repo_group_create_choices], |
|
|||
117 | [x[0] for x in c.user_group_create_choices], |
|
106 | [x[0] for x in c.user_group_create_choices], | |
118 | [x[0] for x in c.fork_choices], |
|
107 | [x[0] for x in c.fork_choices], | |
119 | [x[0] for x in c.register_choices], |
|
108 | [x[0] for x in c.register_choices], | |
@@ -123,15 +112,15 b' class PermissionsController(BaseControll' | |||||
123 | form_result = _form.to_python(dict(request.POST)) |
|
112 | form_result = _form.to_python(dict(request.POST)) | |
124 | form_result.update({'perm_user_name': 'default'}) |
|
113 | form_result.update({'perm_user_name': 'default'}) | |
125 | PermissionModel().update(form_result) |
|
114 | PermissionModel().update(form_result) | |
126 | Session().commit() |
|
115 | meta.Session().commit() | |
127 |
|
|
116 | webutils.flash(_('Global permissions updated successfully'), | |
128 | category='success') |
|
117 | category='success') | |
129 |
|
118 | |||
130 | except formencode.Invalid as errors: |
|
119 | except formencode.Invalid as errors: | |
131 | defaults = errors.value |
|
120 | defaults = errors.value | |
132 |
|
121 | |||
133 | return htmlfill.render( |
|
122 | return htmlfill.render( | |
134 | render('admin/permissions/permissions.html'), |
|
123 | base.render('admin/permissions/permissions.html'), | |
135 | defaults=defaults, |
|
124 | defaults=defaults, | |
136 | errors=errors.error_dict or {}, |
|
125 | errors=errors.error_dict or {}, | |
137 | prefix_error=False, |
|
126 | prefix_error=False, | |
@@ -139,12 +128,12 b' class PermissionsController(BaseControll' | |||||
139 | force_defaults=False) |
|
128 | force_defaults=False) | |
140 | except Exception: |
|
129 | except Exception: | |
141 | log.error(traceback.format_exc()) |
|
130 | log.error(traceback.format_exc()) | |
142 |
|
|
131 | webutils.flash(_('Error occurred during update of permissions'), | |
143 | category='error') |
|
132 | category='error') | |
144 |
|
133 | |||
145 | raise HTTPFound(location=url('admin_permissions')) |
|
134 | raise HTTPFound(location=url('admin_permissions')) | |
146 |
|
135 | |||
147 | c.user = User.get_default_user() |
|
136 | c.user = db.User.get_default_user() | |
148 | defaults = {'anonymous': c.user.active} |
|
137 | defaults = {'anonymous': c.user.active} | |
149 |
|
138 | |||
150 | for p in c.user.user_perms: |
|
139 | for p in c.user.user_perms: | |
@@ -157,15 +146,9 b' class PermissionsController(BaseControll' | |||||
157 | if p.permission.permission_name.startswith('usergroup.'): |
|
146 | if p.permission.permission_name.startswith('usergroup.'): | |
158 | defaults['default_user_group_perm'] = p.permission.permission_name |
|
147 | defaults['default_user_group_perm'] = p.permission.permission_name | |
159 |
|
148 | |||
160 | if p.permission.permission_name.startswith('hg.create.write_on_repogroup.'): |
|
|||
161 | defaults['create_on_write'] = p.permission.permission_name |
|
|||
162 |
|
||||
163 | elif p.permission.permission_name.startswith('hg.create.'): |
|
149 | elif p.permission.permission_name.startswith('hg.create.'): | |
164 | defaults['default_repo_create'] = p.permission.permission_name |
|
150 | defaults['default_repo_create'] = p.permission.permission_name | |
165 |
|
151 | |||
166 | if p.permission.permission_name.startswith('hg.repogroup.'): |
|
|||
167 | defaults['default_repo_group_create'] = p.permission.permission_name |
|
|||
168 |
|
||||
169 | if p.permission.permission_name.startswith('hg.usergroup.'): |
|
152 | if p.permission.permission_name.startswith('hg.usergroup.'): | |
170 | defaults['default_user_group_create'] = p.permission.permission_name |
|
153 | defaults['default_user_group_create'] = p.permission.permission_name | |
171 |
|
154 | |||
@@ -179,21 +162,21 b' class PermissionsController(BaseControll' | |||||
179 | defaults['default_fork'] = p.permission.permission_name |
|
162 | defaults['default_fork'] = p.permission.permission_name | |
180 |
|
163 | |||
181 | return htmlfill.render( |
|
164 | return htmlfill.render( | |
182 | render('admin/permissions/permissions.html'), |
|
165 | base.render('admin/permissions/permissions.html'), | |
183 | defaults=defaults, |
|
166 | defaults=defaults, | |
184 | encoding="UTF-8", |
|
167 | encoding="UTF-8", | |
185 | force_defaults=False) |
|
168 | force_defaults=False) | |
186 |
|
169 | |||
187 | def permission_ips(self): |
|
170 | def permission_ips(self): | |
188 | c.active = 'ips' |
|
171 | c.active = 'ips' | |
189 | c.user = User.get_default_user() |
|
172 | c.user = db.User.get_default_user() | |
190 | c.user_ip_map = UserIpMap.query() \ |
|
173 | c.user_ip_map = db.UserIpMap.query() \ | |
191 | .filter(UserIpMap.user == c.user).all() |
|
174 | .filter(db.UserIpMap.user == c.user).all() | |
192 |
|
175 | |||
193 | return render('admin/permissions/permissions.html') |
|
176 | return base.render('admin/permissions/permissions.html') | |
194 |
|
177 | |||
195 | def permission_perms(self): |
|
178 | def permission_perms(self): | |
196 | c.active = 'perms' |
|
179 | c.active = 'perms' | |
197 | c.user = User.get_default_user() |
|
180 | c.user = db.User.get_default_user() | |
198 | c.perm_user = AuthUser(dbuser=c.user) |
|
181 | c.perm_user = AuthUser(dbuser=c.user) | |
199 | return render('admin/permissions/permissions.html') |
|
182 | return base.render('admin/permissions/permissions.html') |
@@ -36,14 +36,13 b' from tg.i18n import ugettext as _' | |||||
36 | from tg.i18n import ungettext |
|
36 | from tg.i18n import ungettext | |
37 | from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound |
|
37 | from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound | |
38 |
|
38 | |||
39 |
from kallithea.con |
|
39 | from kallithea.controllers import base | |
40 |
from kallithea.lib import |
|
40 | from kallithea.lib import webutils | |
41 | from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired |
|
41 | from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired | |
42 | from kallithea.lib.base import BaseController, render |
|
|||
43 | from kallithea.lib.utils2 import safe_int |
|
42 | from kallithea.lib.utils2 import safe_int | |
44 | from kallithea.model.db import RepoGroup, Repository |
|
43 | from kallithea.lib.webutils import url | |
|
44 | from kallithea.model import db, meta | |||
45 | from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm |
|
45 | from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm | |
46 | from kallithea.model.meta import Session |
|
|||
47 | from kallithea.model.repo import RepoModel |
|
46 | from kallithea.model.repo import RepoModel | |
48 | from kallithea.model.repo_group import RepoGroupModel |
|
47 | from kallithea.model.repo_group import RepoGroupModel | |
49 | from kallithea.model.scm import AvailableRepoGroupChoices, RepoGroupList |
|
48 | from kallithea.model.scm import AvailableRepoGroupChoices, RepoGroupList | |
@@ -52,7 +51,7 b' from kallithea.model.scm import Availabl' | |||||
52 | log = logging.getLogger(__name__) |
|
51 | log = logging.getLogger(__name__) | |
53 |
|
52 | |||
54 |
|
53 | |||
55 | class RepoGroupsController(BaseController): |
|
54 | class RepoGroupsController(base.BaseController): | |
56 |
|
55 | |||
57 | @LoginRequired(allow_default_user=True) |
|
56 | @LoginRequired(allow_default_user=True) | |
58 | def _before(self, *args, **kwargs): |
|
57 | def _before(self, *args, **kwargs): | |
@@ -63,7 +62,7 b' class RepoGroupsController(BaseControlle' | |||||
63 | exclude is used for not moving group to itself TODO: also exclude descendants |
|
62 | exclude is used for not moving group to itself TODO: also exclude descendants | |
64 | Note: only admin can create top level groups |
|
63 | Note: only admin can create top level groups | |
65 | """ |
|
64 | """ | |
66 |
repo_groups = AvailableRepoGroupChoices( |
|
65 | repo_groups = AvailableRepoGroupChoices('admin', extras) | |
67 | exclude_group_ids = set(rg.group_id for rg in exclude) |
|
66 | exclude_group_ids = set(rg.group_id for rg in exclude) | |
68 | c.repo_groups = [rg for rg in repo_groups |
|
67 | c.repo_groups = [rg for rg in repo_groups | |
69 | if rg[0] not in exclude_group_ids] |
|
68 | if rg[0] not in exclude_group_ids] | |
@@ -74,7 +73,7 b' class RepoGroupsController(BaseControlle' | |||||
74 |
|
73 | |||
75 | :param group_id: |
|
74 | :param group_id: | |
76 | """ |
|
75 | """ | |
77 | repo_group = RepoGroup.get_or_404(group_id) |
|
76 | repo_group = db.RepoGroup.get_or_404(group_id) | |
78 | data = repo_group.get_dict() |
|
77 | data = repo_group.get_dict() | |
79 | data['group_name'] = repo_group.name |
|
78 | data['group_name'] = repo_group.name | |
80 |
|
79 | |||
@@ -98,7 +97,7 b' class RepoGroupsController(BaseControlle' | |||||
98 | return False |
|
97 | return False | |
99 |
|
98 | |||
100 | def index(self, format='html'): |
|
99 | def index(self, format='html'): | |
101 | _list = RepoGroup.query(sorted=True).all() |
|
100 | _list = db.RepoGroup.query(sorted=True).all() | |
102 | group_iter = RepoGroupList(_list, perm_level='admin') |
|
101 | group_iter = RepoGroupList(_list, perm_level='admin') | |
103 | repo_groups_data = [] |
|
102 | repo_groups_data = [] | |
104 | _tmpl_lookup = app_globals.mako_lookup |
|
103 | _tmpl_lookup = app_globals.mako_lookup | |
@@ -106,22 +105,22 b' class RepoGroupsController(BaseControlle' | |||||
106 |
|
105 | |||
107 | def repo_group_name(repo_group_name, children_groups): |
|
106 | def repo_group_name(repo_group_name, children_groups): | |
108 | return template.get_def("repo_group_name") \ |
|
107 | return template.get_def("repo_group_name") \ | |
109 |
.render_unicode(repo_group_name, children_groups, _=_, |
|
108 | .render_unicode(repo_group_name, children_groups, _=_, webutils=webutils, c=c) | |
110 |
|
109 | |||
111 | def repo_group_actions(repo_group_id, repo_group_name, gr_count): |
|
110 | def repo_group_actions(repo_group_id, repo_group_name, gr_count): | |
112 | return template.get_def("repo_group_actions") \ |
|
111 | return template.get_def("repo_group_actions") \ | |
113 |
.render_unicode(repo_group_id, repo_group_name, gr_count, _=_, |
|
112 | .render_unicode(repo_group_id, repo_group_name, gr_count, _=_, webutils=webutils, c=c, | |
114 | ungettext=ungettext) |
|
113 | ungettext=ungettext) | |
115 |
|
114 | |||
116 | for repo_gr in group_iter: |
|
115 | for repo_gr in group_iter: | |
117 | children_groups = [g.name for g in repo_gr.parents] + [repo_gr.name] |
|
116 | children_groups = [g.name for g in repo_gr.parents] + [repo_gr.name] | |
118 | repo_count = repo_gr.repositories.count() |
|
117 | repo_count = repo_gr.repositories.count() | |
119 | repo_groups_data.append({ |
|
118 | repo_groups_data.append({ | |
120 |
"raw_name": |
|
119 | "raw_name": webutils.escape(repo_gr.group_name), | |
121 | "group_name": repo_group_name(repo_gr.group_name, children_groups), |
|
120 | "group_name": repo_group_name(repo_gr.group_name, children_groups), | |
122 |
"desc": |
|
121 | "desc": webutils.escape(repo_gr.group_description), | |
123 | "repos": repo_count, |
|
122 | "repos": repo_count, | |
124 |
"owner": |
|
123 | "owner": repo_gr.owner.username, | |
125 | "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name, |
|
124 | "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name, | |
126 | repo_count) |
|
125 | repo_count) | |
127 | }) |
|
126 | }) | |
@@ -132,7 +131,7 b' class RepoGroupsController(BaseControlle' | |||||
132 | "records": repo_groups_data |
|
131 | "records": repo_groups_data | |
133 | } |
|
132 | } | |
134 |
|
133 | |||
135 | return render('admin/repo_groups/repo_groups.html') |
|
134 | return base.render('admin/repo_groups/repo_groups.html') | |
136 |
|
135 | |||
137 | def create(self): |
|
136 | def create(self): | |
138 | self.__load_defaults() |
|
137 | self.__load_defaults() | |
@@ -150,11 +149,11 b' class RepoGroupsController(BaseControlle' | |||||
150 | owner=request.authuser.user_id, # TODO: make editable |
|
149 | owner=request.authuser.user_id, # TODO: make editable | |
151 | copy_permissions=form_result['group_copy_permissions'] |
|
150 | copy_permissions=form_result['group_copy_permissions'] | |
152 | ) |
|
151 | ) | |
153 | Session().commit() |
|
152 | meta.Session().commit() | |
154 | # TODO: in future action_logger(, '', '', '') |
|
153 | # TODO: in future action_logger(, '', '', '') | |
155 | except formencode.Invalid as errors: |
|
154 | except formencode.Invalid as errors: | |
156 | return htmlfill.render( |
|
155 | return htmlfill.render( | |
157 | render('admin/repo_groups/repo_group_add.html'), |
|
156 | base.render('admin/repo_groups/repo_group_add.html'), | |
158 | defaults=errors.value, |
|
157 | defaults=errors.value, | |
159 | errors=errors.error_dict or {}, |
|
158 | errors=errors.error_dict or {}, | |
160 | prefix_error=False, |
|
159 | prefix_error=False, | |
@@ -162,14 +161,14 b' class RepoGroupsController(BaseControlle' | |||||
162 | force_defaults=False) |
|
161 | force_defaults=False) | |
163 | except Exception: |
|
162 | except Exception: | |
164 | log.error(traceback.format_exc()) |
|
163 | log.error(traceback.format_exc()) | |
165 |
|
|
164 | webutils.flash(_('Error occurred during creation of repository group %s') | |
166 | % request.POST.get('group_name'), category='error') |
|
165 | % request.POST.get('group_name'), category='error') | |
167 | if form_result is None: |
|
166 | if form_result is None: | |
168 | raise |
|
167 | raise | |
169 | parent_group_id = form_result['parent_group_id'] |
|
168 | parent_group_id = form_result['parent_group_id'] | |
170 | # TODO: maybe we should get back to the main view, not the admin one |
|
169 | # TODO: maybe we should get back to the main view, not the admin one | |
171 | raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id)) |
|
170 | raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id)) | |
172 |
|
|
171 | webutils.flash(_('Created repository group %s') % gr.group_name, | |
173 | category='success') |
|
172 | category='success') | |
174 | raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name)) |
|
173 | raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name)) | |
175 |
|
174 | |||
@@ -181,7 +180,7 b' class RepoGroupsController(BaseControlle' | |||||
181 | else: |
|
180 | else: | |
182 | # we pass in parent group into creation form, thus we know |
|
181 | # we pass in parent group into creation form, thus we know | |
183 | # what would be the group, we can check perms here ! |
|
182 | # what would be the group, we can check perms here ! | |
184 | group = RepoGroup.get(parent_group_id) if parent_group_id else None |
|
183 | group = db.RepoGroup.get(parent_group_id) if parent_group_id else None | |
185 | group_name = group.group_name if group else None |
|
184 | group_name = group.group_name if group else None | |
186 | if HasRepoGroupPermissionLevel('admin')(group_name, 'group create'): |
|
185 | if HasRepoGroupPermissionLevel('admin')(group_name, 'group create'): | |
187 | pass |
|
186 | pass | |
@@ -190,7 +189,7 b' class RepoGroupsController(BaseControlle' | |||||
190 |
|
189 | |||
191 | self.__load_defaults() |
|
190 | self.__load_defaults() | |
192 | return htmlfill.render( |
|
191 | return htmlfill.render( | |
193 | render('admin/repo_groups/repo_group_add.html'), |
|
192 | base.render('admin/repo_groups/repo_group_add.html'), | |
194 | defaults={'parent_group_id': parent_group_id}, |
|
193 | defaults={'parent_group_id': parent_group_id}, | |
195 | errors={}, |
|
194 | errors={}, | |
196 | prefix_error=False, |
|
195 | prefix_error=False, | |
@@ -199,7 +198,7 b' class RepoGroupsController(BaseControlle' | |||||
199 |
|
198 | |||
200 | @HasRepoGroupPermissionLevelDecorator('admin') |
|
199 | @HasRepoGroupPermissionLevelDecorator('admin') | |
201 | def update(self, group_name): |
|
200 | def update(self, group_name): | |
202 | c.repo_group = RepoGroup.guess_instance(group_name) |
|
201 | c.repo_group = db.RepoGroup.guess_instance(group_name) | |
203 | self.__load_defaults(extras=[c.repo_group.parent_group], |
|
202 | self.__load_defaults(extras=[c.repo_group.parent_group], | |
204 | exclude=[c.repo_group]) |
|
203 | exclude=[c.repo_group]) | |
205 |
|
204 | |||
@@ -221,8 +220,8 b' class RepoGroupsController(BaseControlle' | |||||
221 | form_result = repo_group_form.to_python(dict(request.POST)) |
|
220 | form_result = repo_group_form.to_python(dict(request.POST)) | |
222 |
|
221 | |||
223 | new_gr = RepoGroupModel().update(group_name, form_result) |
|
222 | new_gr = RepoGroupModel().update(group_name, form_result) | |
224 | Session().commit() |
|
223 | meta.Session().commit() | |
225 |
|
|
224 | webutils.flash(_('Updated repository group %s') | |
226 | % form_result['group_name'], category='success') |
|
225 | % form_result['group_name'], category='success') | |
227 | # we now have new name ! |
|
226 | # we now have new name ! | |
228 | group_name = new_gr.group_name |
|
227 | group_name = new_gr.group_name | |
@@ -230,7 +229,7 b' class RepoGroupsController(BaseControlle' | |||||
230 | except formencode.Invalid as errors: |
|
229 | except formencode.Invalid as errors: | |
231 | c.active = 'settings' |
|
230 | c.active = 'settings' | |
232 | return htmlfill.render( |
|
231 | return htmlfill.render( | |
233 | render('admin/repo_groups/repo_group_edit.html'), |
|
232 | base.render('admin/repo_groups/repo_group_edit.html'), | |
234 | defaults=errors.value, |
|
233 | defaults=errors.value, | |
235 | errors=errors.error_dict or {}, |
|
234 | errors=errors.error_dict or {}, | |
236 | prefix_error=False, |
|
235 | prefix_error=False, | |
@@ -238,35 +237,35 b' class RepoGroupsController(BaseControlle' | |||||
238 | force_defaults=False) |
|
237 | force_defaults=False) | |
239 | except Exception: |
|
238 | except Exception: | |
240 | log.error(traceback.format_exc()) |
|
239 | log.error(traceback.format_exc()) | |
241 |
|
|
240 | webutils.flash(_('Error occurred during update of repository group %s') | |
242 | % request.POST.get('group_name'), category='error') |
|
241 | % request.POST.get('group_name'), category='error') | |
243 |
|
242 | |||
244 | raise HTTPFound(location=url('edit_repo_group', group_name=group_name)) |
|
243 | raise HTTPFound(location=url('edit_repo_group', group_name=group_name)) | |
245 |
|
244 | |||
246 | @HasRepoGroupPermissionLevelDecorator('admin') |
|
245 | @HasRepoGroupPermissionLevelDecorator('admin') | |
247 | def delete(self, group_name): |
|
246 | def delete(self, group_name): | |
248 | gr = c.repo_group = RepoGroup.guess_instance(group_name) |
|
247 | gr = c.repo_group = db.RepoGroup.guess_instance(group_name) | |
249 | repos = gr.repositories.all() |
|
248 | repos = gr.repositories.all() | |
250 | if repos: |
|
249 | if repos: | |
251 |
|
|
250 | webutils.flash(_('This group contains %s repositories and cannot be ' | |
252 | 'deleted') % len(repos), category='warning') |
|
251 | 'deleted') % len(repos), category='warning') | |
253 | raise HTTPFound(location=url('repos_groups')) |
|
252 | raise HTTPFound(location=url('repos_groups')) | |
254 |
|
253 | |||
255 | children = gr.children.all() |
|
254 | children = gr.children.all() | |
256 | if children: |
|
255 | if children: | |
257 |
|
|
256 | webutils.flash(_('This group contains %s subgroups and cannot be deleted' | |
258 | % (len(children))), category='warning') |
|
257 | % (len(children))), category='warning') | |
259 | raise HTTPFound(location=url('repos_groups')) |
|
258 | raise HTTPFound(location=url('repos_groups')) | |
260 |
|
259 | |||
261 | try: |
|
260 | try: | |
262 | RepoGroupModel().delete(group_name) |
|
261 | RepoGroupModel().delete(group_name) | |
263 | Session().commit() |
|
262 | meta.Session().commit() | |
264 |
|
|
263 | webutils.flash(_('Removed repository group %s') % group_name, | |
265 | category='success') |
|
264 | category='success') | |
266 | # TODO: in future action_logger(, '', '', '') |
|
265 | # TODO: in future action_logger(, '', '', '') | |
267 | except Exception: |
|
266 | except Exception: | |
268 | log.error(traceback.format_exc()) |
|
267 | log.error(traceback.format_exc()) | |
269 |
|
|
268 | webutils.flash(_('Error occurred during deletion of repository group %s') | |
270 | % group_name, category='error') |
|
269 | % group_name, category='error') | |
271 |
|
270 | |||
272 | if gr.parent_group: |
|
271 | if gr.parent_group: | |
@@ -279,7 +278,7 b' class RepoGroupsController(BaseControlle' | |||||
279 | the group by id view instead |
|
278 | the group by id view instead | |
280 | """ |
|
279 | """ | |
281 | group_name = group_name.rstrip('/') |
|
280 | group_name = group_name.rstrip('/') | |
282 | id_ = RepoGroup.get_by_group_name(group_name) |
|
281 | id_ = db.RepoGroup.get_by_group_name(group_name) | |
283 | if id_: |
|
282 | if id_: | |
284 | return self.show(group_name) |
|
283 | return self.show(group_name) | |
285 | raise HTTPNotFound |
|
284 | raise HTTPNotFound | |
@@ -288,29 +287,29 b' class RepoGroupsController(BaseControlle' | |||||
288 | def show(self, group_name): |
|
287 | def show(self, group_name): | |
289 | c.active = 'settings' |
|
288 | c.active = 'settings' | |
290 |
|
289 | |||
291 | c.group = c.repo_group = RepoGroup.guess_instance(group_name) |
|
290 | c.group = c.repo_group = db.RepoGroup.guess_instance(group_name) | |
292 |
|
291 | |||
293 | groups = RepoGroup.query(sorted=True).filter_by(parent_group=c.group).all() |
|
292 | groups = db.RepoGroup.query(sorted=True).filter_by(parent_group=c.group).all() | |
294 | repo_groups_list = self.scm_model.get_repo_groups(groups) |
|
293 | repo_groups_list = self.scm_model.get_repo_groups(groups) | |
295 |
|
294 | |||
296 | repos_list = Repository.query(sorted=True).filter_by(group=c.group).all() |
|
295 | repos_list = db.Repository.query(sorted=True).filter_by(group=c.group).all() | |
297 | c.data = RepoModel().get_repos_as_dict(repos_list, |
|
296 | c.data = RepoModel().get_repos_as_dict(repos_list, | |
298 | repo_groups_list=repo_groups_list, |
|
297 | repo_groups_list=repo_groups_list, | |
299 | short_name=True) |
|
298 | short_name=True) | |
300 |
|
299 | |||
301 | return render('admin/repo_groups/repo_group_show.html') |
|
300 | return base.render('admin/repo_groups/repo_group_show.html') | |
302 |
|
301 | |||
303 | @HasRepoGroupPermissionLevelDecorator('admin') |
|
302 | @HasRepoGroupPermissionLevelDecorator('admin') | |
304 | def edit(self, group_name): |
|
303 | def edit(self, group_name): | |
305 | c.active = 'settings' |
|
304 | c.active = 'settings' | |
306 |
|
305 | |||
307 | c.repo_group = RepoGroup.guess_instance(group_name) |
|
306 | c.repo_group = db.RepoGroup.guess_instance(group_name) | |
308 | self.__load_defaults(extras=[c.repo_group.parent_group], |
|
307 | self.__load_defaults(extras=[c.repo_group.parent_group], | |
309 | exclude=[c.repo_group]) |
|
308 | exclude=[c.repo_group]) | |
310 | defaults = self.__load_data(c.repo_group.group_id) |
|
309 | defaults = self.__load_data(c.repo_group.group_id) | |
311 |
|
310 | |||
312 | return htmlfill.render( |
|
311 | return htmlfill.render( | |
313 | render('admin/repo_groups/repo_group_edit.html'), |
|
312 | base.render('admin/repo_groups/repo_group_edit.html'), | |
314 | defaults=defaults, |
|
313 | defaults=defaults, | |
315 | encoding="UTF-8", |
|
314 | encoding="UTF-8", | |
316 | force_defaults=False |
|
315 | force_defaults=False | |
@@ -319,19 +318,19 b' class RepoGroupsController(BaseControlle' | |||||
319 | @HasRepoGroupPermissionLevelDecorator('admin') |
|
318 | @HasRepoGroupPermissionLevelDecorator('admin') | |
320 | def edit_repo_group_advanced(self, group_name): |
|
319 | def edit_repo_group_advanced(self, group_name): | |
321 | c.active = 'advanced' |
|
320 | c.active = 'advanced' | |
322 | c.repo_group = RepoGroup.guess_instance(group_name) |
|
321 | c.repo_group = db.RepoGroup.guess_instance(group_name) | |
323 |
|
322 | |||
324 | return render('admin/repo_groups/repo_group_edit.html') |
|
323 | return base.render('admin/repo_groups/repo_group_edit.html') | |
325 |
|
324 | |||
326 | @HasRepoGroupPermissionLevelDecorator('admin') |
|
325 | @HasRepoGroupPermissionLevelDecorator('admin') | |
327 | def edit_repo_group_perms(self, group_name): |
|
326 | def edit_repo_group_perms(self, group_name): | |
328 | c.active = 'perms' |
|
327 | c.active = 'perms' | |
329 | c.repo_group = RepoGroup.guess_instance(group_name) |
|
328 | c.repo_group = db.RepoGroup.guess_instance(group_name) | |
330 | self.__load_defaults() |
|
329 | self.__load_defaults() | |
331 | defaults = self.__load_data(c.repo_group.group_id) |
|
330 | defaults = self.__load_data(c.repo_group.group_id) | |
332 |
|
331 | |||
333 | return htmlfill.render( |
|
332 | return htmlfill.render( | |
334 | render('admin/repo_groups/repo_group_edit.html'), |
|
333 | base.render('admin/repo_groups/repo_group_edit.html'), | |
335 | defaults=defaults, |
|
334 | defaults=defaults, | |
336 | encoding="UTF-8", |
|
335 | encoding="UTF-8", | |
337 | force_defaults=False |
|
336 | force_defaults=False | |
@@ -345,13 +344,13 b' class RepoGroupsController(BaseControlle' | |||||
345 | :param group_name: |
|
344 | :param group_name: | |
346 | """ |
|
345 | """ | |
347 |
|
346 | |||
348 | c.repo_group = RepoGroup.guess_instance(group_name) |
|
347 | c.repo_group = db.RepoGroup.guess_instance(group_name) | |
349 | valid_recursive_choices = ['none', 'repos', 'groups', 'all'] |
|
348 | valid_recursive_choices = ['none', 'repos', 'groups', 'all'] | |
350 | form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST) |
|
349 | form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST) | |
351 | if not request.authuser.is_admin: |
|
350 | if not request.authuser.is_admin: | |
352 | if self._revoke_perms_on_yourself(form_result): |
|
351 | if self._revoke_perms_on_yourself(form_result): | |
353 | msg = _('Cannot revoke permission for yourself as admin') |
|
352 | msg = _('Cannot revoke permission for yourself as admin') | |
354 |
|
|
353 | webutils.flash(msg, category='warning') | |
355 | raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name)) |
|
354 | raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name)) | |
356 | recursive = form_result['recursive'] |
|
355 | recursive = form_result['recursive'] | |
357 | # iterate over all members(if in recursive mode) of this groups and |
|
356 | # iterate over all members(if in recursive mode) of this groups and | |
@@ -364,8 +363,8 b' class RepoGroupsController(BaseControlle' | |||||
364 | # TODO: implement this |
|
363 | # TODO: implement this | |
365 | #action_logger(request.authuser, 'admin_changed_repo_permissions', |
|
364 | #action_logger(request.authuser, 'admin_changed_repo_permissions', | |
366 | # repo_name, request.ip_addr) |
|
365 | # repo_name, request.ip_addr) | |
367 | Session().commit() |
|
366 | meta.Session().commit() | |
368 |
|
|
367 | webutils.flash(_('Repository group permissions updated'), category='success') | |
369 | raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name)) |
|
368 | raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name)) | |
370 |
|
369 | |||
371 | @HasRepoGroupPermissionLevelDecorator('admin') |
|
370 | @HasRepoGroupPermissionLevelDecorator('admin') | |
@@ -381,7 +380,7 b' class RepoGroupsController(BaseControlle' | |||||
381 | if not request.authuser.is_admin: |
|
380 | if not request.authuser.is_admin: | |
382 | if obj_type == 'user' and request.authuser.user_id == obj_id: |
|
381 | if obj_type == 'user' and request.authuser.user_id == obj_id: | |
383 | msg = _('Cannot revoke permission for yourself as admin') |
|
382 | msg = _('Cannot revoke permission for yourself as admin') | |
384 |
|
|
383 | webutils.flash(msg, category='warning') | |
385 | raise Exception('revoke admin permission on self') |
|
384 | raise Exception('revoke admin permission on self') | |
386 | recursive = request.POST.get('recursive', 'none') |
|
385 | recursive = request.POST.get('recursive', 'none') | |
387 | if obj_type == 'user': |
|
386 | if obj_type == 'user': | |
@@ -394,9 +393,9 b' class RepoGroupsController(BaseControlle' | |||||
394 | obj_type='user_group', |
|
393 | obj_type='user_group', | |
395 | recursive=recursive) |
|
394 | recursive=recursive) | |
396 |
|
395 | |||
397 | Session().commit() |
|
396 | meta.Session().commit() | |
398 | except Exception: |
|
397 | except Exception: | |
399 | log.error(traceback.format_exc()) |
|
398 | log.error(traceback.format_exc()) | |
400 |
|
|
399 | webutils.flash(_('An error occurred during revoking of permission'), | |
401 | category='error') |
|
400 | category='error') | |
402 | raise HTTPInternalServerError() |
|
401 | raise HTTPInternalServerError() |
@@ -28,7 +28,6 b' Original author and date, and relevant c' | |||||
28 | import logging |
|
28 | import logging | |
29 | import traceback |
|
29 | import traceback | |
30 |
|
30 | |||
31 | import celery.result |
|
|||
32 | import formencode |
|
31 | import formencode | |
33 | from formencode import htmlfill |
|
32 | from formencode import htmlfill | |
34 | from tg import request |
|
33 | from tg import request | |
@@ -37,17 +36,15 b' from tg.i18n import ugettext as _' | |||||
37 | from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound |
|
36 | from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound | |
38 |
|
37 | |||
39 | import kallithea |
|
38 | import kallithea | |
40 |
from kallithea.con |
|
39 | from kallithea.controllers import base | |
41 |
from kallithea.lib import |
|
40 | from kallithea.lib import webutils | |
42 |
from kallithea.lib.auth import |
|
41 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired, NotAnonymous | |
43 | from kallithea.lib.base import BaseRepoController, jsonify, render |
|
|||
44 | from kallithea.lib.exceptions import AttachedForksError |
|
42 | from kallithea.lib.exceptions import AttachedForksError | |
45 | from kallithea.lib.utils import action_logger |
|
|||
46 | from kallithea.lib.utils2 import safe_int |
|
43 | from kallithea.lib.utils2 import safe_int | |
47 | from kallithea.lib.vcs import RepositoryError |
|
44 | from kallithea.lib.vcs import RepositoryError | |
48 | from kallithea.model.db import RepoGroup, Repository, RepositoryField, Setting, UserFollowing |
|
45 | from kallithea.lib.webutils import url | |
|
46 | from kallithea.model import db, meta, userlog | |||
49 | from kallithea.model.forms import RepoFieldForm, RepoForm, RepoPermsForm |
|
47 | from kallithea.model.forms import RepoFieldForm, RepoForm, RepoPermsForm | |
50 | from kallithea.model.meta import Session |
|
|||
51 | from kallithea.model.repo import RepoModel |
|
48 | from kallithea.model.repo import RepoModel | |
52 | from kallithea.model.scm import AvailableRepoGroupChoices, RepoList, ScmModel |
|
49 | from kallithea.model.scm import AvailableRepoGroupChoices, RepoList, ScmModel | |
53 |
|
50 | |||
@@ -55,12 +52,7 b' from kallithea.model.scm import Availabl' | |||||
55 | log = logging.getLogger(__name__) |
|
52 | log = logging.getLogger(__name__) | |
56 |
|
53 | |||
57 |
|
54 | |||
58 | class ReposController(BaseRepoController): |
|
55 | class ReposController(base.BaseRepoController): | |
59 | """ |
|
|||
60 | REST Controller styled on the Atom Publishing Protocol""" |
|
|||
61 | # To properly map this controller, ensure your config/routing.py |
|
|||
62 | # file has a resource setup: |
|
|||
63 | # map.resource('repo', 'repos') |
|
|||
64 |
|
56 | |||
65 | @LoginRequired(allow_default_user=True) |
|
57 | @LoginRequired(allow_default_user=True) | |
66 | def _before(self, *args, **kwargs): |
|
58 | def _before(self, *args, **kwargs): | |
@@ -70,20 +62,14 b' class ReposController(BaseRepoController' | |||||
70 | repo_obj = c.db_repo |
|
62 | repo_obj = c.db_repo | |
71 |
|
63 | |||
72 | if repo_obj is None: |
|
64 | if repo_obj is None: | |
73 | h.not_mapped_error(c.repo_name) |
|
65 | raise HTTPNotFound() | |
74 | raise HTTPFound(location=url('repos')) |
|
|||
75 |
|
66 | |||
76 | return repo_obj |
|
67 | return repo_obj | |
77 |
|
68 | |||
78 | def __load_defaults(self, repo=None): |
|
69 | def __load_defaults(self, repo=None): | |
79 | top_perms = ['hg.create.repository'] |
|
|||
80 | if HasPermissionAny('hg.create.write_on_repogroup.true')(): |
|
|||
81 | repo_group_perm_level = 'write' |
|
|||
82 | else: |
|
|||
83 | repo_group_perm_level = 'admin' |
|
|||
84 | extras = [] if repo is None else [repo.group] |
|
70 | extras = [] if repo is None else [repo.group] | |
85 |
|
71 | |||
86 |
c.repo_groups = AvailableRepoGroupChoices( |
|
72 | c.repo_groups = AvailableRepoGroupChoices('write', extras) | |
87 |
|
73 | |||
88 | c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo) |
|
74 | c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo) | |
89 |
|
75 | |||
@@ -101,13 +87,13 b' class ReposController(BaseRepoController' | |||||
101 | return defaults |
|
87 | return defaults | |
102 |
|
88 | |||
103 | def index(self, format='html'): |
|
89 | def index(self, format='html'): | |
104 | repos_list = RepoList(Repository.query(sorted=True).all(), perm_level='admin') |
|
90 | repos_list = RepoList(db.Repository.query(sorted=True).all(), perm_level='admin') | |
105 | # the repo list will be filtered to only show repos where the user has read permissions |
|
91 | # the repo list will be filtered to only show repos where the user has read permissions | |
106 | repos_data = RepoModel().get_repos_as_dict(repos_list, admin=True) |
|
92 | repos_data = RepoModel().get_repos_as_dict(repos_list, admin=True) | |
107 | # data used to render the grid |
|
93 | # data used to render the grid | |
108 | c.data = repos_data |
|
94 | c.data = repos_data | |
109 |
|
95 | |||
110 | return render('admin/repos/repos.html') |
|
96 | return base.render('admin/repos/repos.html') | |
111 |
|
97 | |||
112 | @NotAnonymous() |
|
98 | @NotAnonymous() | |
113 | def create(self): |
|
99 | def create(self): | |
@@ -120,7 +106,7 b' class ReposController(BaseRepoController' | |||||
120 | except formencode.Invalid as errors: |
|
106 | except formencode.Invalid as errors: | |
121 | log.info(errors) |
|
107 | log.info(errors) | |
122 | return htmlfill.render( |
|
108 | return htmlfill.render( | |
123 | render('admin/repos/repo_add.html'), |
|
109 | base.render('admin/repos/repo_add.html'), | |
124 | defaults=errors.value, |
|
110 | defaults=errors.value, | |
125 | errors=errors.error_dict or {}, |
|
111 | errors=errors.error_dict or {}, | |
126 | prefix_error=False, |
|
112 | prefix_error=False, | |
@@ -130,18 +116,17 b' class ReposController(BaseRepoController' | |||||
130 | try: |
|
116 | try: | |
131 | # create is done sometimes async on celery, db transaction |
|
117 | # create is done sometimes async on celery, db transaction | |
132 | # management is handled there. |
|
118 | # management is handled there. | |
133 |
|
|
119 | RepoModel().create(form_result, request.authuser.user_id) | |
134 | task_id = task.task_id |
|
|||
135 | except Exception: |
|
120 | except Exception: | |
136 | log.error(traceback.format_exc()) |
|
121 | log.error(traceback.format_exc()) | |
137 | msg = (_('Error creating repository %s') |
|
122 | msg = (_('Error creating repository %s') | |
138 | % form_result.get('repo_name')) |
|
123 | % form_result.get('repo_name')) | |
139 |
|
|
124 | webutils.flash(msg, category='error') | |
140 | raise HTTPFound(location=url('home')) |
|
125 | raise HTTPFound(location=url('home')) | |
141 |
|
126 | |||
142 |
raise HTTPFound(location= |
|
127 | raise HTTPFound(location=webutils.url('repo_creating_home', | |
143 | repo_name=form_result['repo_name_full'], |
|
128 | repo_name=form_result['repo_name_full'], | |
144 |
|
|
129 | )) | |
145 |
|
130 | |||
146 | @NotAnonymous() |
|
131 | @NotAnonymous() | |
147 | def create_repository(self): |
|
132 | def create_repository(self): | |
@@ -151,9 +136,9 b' class ReposController(BaseRepoController' | |||||
151 | parent_group = request.GET.get('parent_group') |
|
136 | parent_group = request.GET.get('parent_group') | |
152 |
|
137 | |||
153 | ## apply the defaults from defaults page |
|
138 | ## apply the defaults from defaults page | |
154 | defaults = Setting.get_default_repo_settings(strip_prefix=True) |
|
139 | defaults = db.Setting.get_default_repo_settings(strip_prefix=True) | |
155 | if parent_group: |
|
140 | if parent_group: | |
156 | prg = RepoGroup.get(parent_group) |
|
141 | prg = db.RepoGroup.get(parent_group) | |
157 | if prg is None or not any(rgc[0] == prg.group_id |
|
142 | if prg is None or not any(rgc[0] == prg.group_id | |
158 | for rgc in c.repo_groups): |
|
143 | for rgc in c.repo_groups): | |
159 | raise HTTPForbidden |
|
144 | raise HTTPForbidden | |
@@ -162,7 +147,7 b' class ReposController(BaseRepoController' | |||||
162 | defaults.update({'repo_group': parent_group}) |
|
147 | defaults.update({'repo_group': parent_group}) | |
163 |
|
148 | |||
164 | return htmlfill.render( |
|
149 | return htmlfill.render( | |
165 | render('admin/repos/repo_add.html'), |
|
150 | base.render('admin/repos/repo_add.html'), | |
166 | defaults=defaults, |
|
151 | defaults=defaults, | |
167 | errors={}, |
|
152 | errors={}, | |
168 | prefix_error=False, |
|
153 | prefix_error=False, | |
@@ -172,39 +157,30 b' class ReposController(BaseRepoController' | |||||
172 | @LoginRequired() |
|
157 | @LoginRequired() | |
173 | def repo_creating(self, repo_name): |
|
158 | def repo_creating(self, repo_name): | |
174 | c.repo = repo_name |
|
159 | c.repo = repo_name | |
175 | c.task_id = request.GET.get('task_id') |
|
|||
176 | if not c.repo: |
|
160 | if not c.repo: | |
177 | raise HTTPNotFound() |
|
161 | raise HTTPNotFound() | |
178 | return render('admin/repos/repo_creating.html') |
|
162 | return base.render('admin/repos/repo_creating.html') | |
179 |
|
163 | |||
180 | @LoginRequired() |
|
164 | @LoginRequired() | |
181 | @jsonify |
|
165 | @base.jsonify | |
182 | def repo_check(self, repo_name): |
|
166 | def repo_check(self, repo_name): | |
183 | c.repo = repo_name |
|
167 | c.repo = repo_name | |
184 | task_id = request.GET.get('task_id') |
|
168 | repo = db.Repository.get_by_repo_name(repo_name) | |
185 |
|
169 | if repo and repo.repo_state == db.Repository.STATE_CREATED: | ||
186 | if task_id and task_id not in ['None']: |
|
|||
187 | if kallithea.CELERY_APP: |
|
|||
188 | task_result = celery.result.AsyncResult(task_id, app=kallithea.CELERY_APP) |
|
|||
189 | if task_result.failed(): |
|
|||
190 | raise HTTPInternalServerError(task_result.traceback) |
|
|||
191 |
|
||||
192 | repo = Repository.get_by_repo_name(repo_name) |
|
|||
193 | if repo and repo.repo_state == Repository.STATE_CREATED: |
|
|||
194 | if repo.clone_uri: |
|
170 | if repo.clone_uri: | |
195 |
|
|
171 | webutils.flash(_('Created repository %s from %s') | |
196 | % (repo.repo_name, repo.clone_uri_hidden), category='success') |
|
172 | % (repo.repo_name, repo.clone_uri_hidden), category='success') | |
197 | else: |
|
173 | else: | |
198 |
repo_url = |
|
174 | repo_url = webutils.link_to(repo.repo_name, | |
199 |
|
|
175 | webutils.url('summary_home', | |
200 | repo_name=repo.repo_name)) |
|
176 | repo_name=repo.repo_name)) | |
201 | fork = repo.fork |
|
177 | fork = repo.fork | |
202 | if fork is not None: |
|
178 | if fork is not None: | |
203 | fork_name = fork.repo_name |
|
179 | fork_name = fork.repo_name | |
204 |
|
|
180 | webutils.flash(webutils.HTML(_('Forked repository %s as %s')) | |
205 | % (fork_name, repo_url), category='success') |
|
181 | % (fork_name, repo_url), category='success') | |
206 | else: |
|
182 | else: | |
207 |
|
|
183 | webutils.flash(webutils.HTML(_('Created repository %s')) % repo_url, | |
208 | category='success') |
|
184 | category='success') | |
209 | return {'result': True} |
|
185 | return {'result': True} | |
210 | return {'result': False} |
|
186 | return {'result': False} | |
@@ -214,12 +190,12 b' class ReposController(BaseRepoController' | |||||
214 | c.repo_info = self._load_repo() |
|
190 | c.repo_info = self._load_repo() | |
215 | self.__load_defaults(c.repo_info) |
|
191 | self.__load_defaults(c.repo_info) | |
216 | c.active = 'settings' |
|
192 | c.active = 'settings' | |
217 | c.repo_fields = RepositoryField.query() \ |
|
193 | c.repo_fields = db.RepositoryField.query() \ | |
218 | .filter(RepositoryField.repository == c.repo_info).all() |
|
194 | .filter(db.RepositoryField.repository == c.repo_info).all() | |
219 |
|
195 | |||
220 | repo_model = RepoModel() |
|
196 | repo_model = RepoModel() | |
221 | changed_name = repo_name |
|
197 | changed_name = repo_name | |
222 | repo = Repository.get_by_repo_name(repo_name) |
|
198 | repo = db.Repository.get_by_repo_name(repo_name) | |
223 | old_data = { |
|
199 | old_data = { | |
224 | 'repo_name': repo_name, |
|
200 | 'repo_name': repo_name, | |
225 | 'repo_group': repo.group.get_dict() if repo.group else {}, |
|
201 | 'repo_group': repo.group.get_dict() if repo.group else {}, | |
@@ -233,18 +209,18 b' class ReposController(BaseRepoController' | |||||
233 | form_result = _form.to_python(dict(request.POST)) |
|
209 | form_result = _form.to_python(dict(request.POST)) | |
234 | repo = repo_model.update(repo_name, **form_result) |
|
210 | repo = repo_model.update(repo_name, **form_result) | |
235 | ScmModel().mark_for_invalidation(repo_name) |
|
211 | ScmModel().mark_for_invalidation(repo_name) | |
236 |
|
|
212 | webutils.flash(_('Repository %s updated successfully') % repo_name, | |
237 | category='success') |
|
213 | category='success') | |
238 | changed_name = repo.repo_name |
|
214 | changed_name = repo.repo_name | |
239 | action_logger(request.authuser, 'admin_updated_repo', |
|
215 | userlog.action_logger(request.authuser, 'admin_updated_repo', | |
240 | changed_name, request.ip_addr) |
|
216 | changed_name, request.ip_addr) | |
241 | Session().commit() |
|
217 | meta.Session().commit() | |
242 | except formencode.Invalid as errors: |
|
218 | except formencode.Invalid as errors: | |
243 | log.info(errors) |
|
219 | log.info(errors) | |
244 | defaults = self.__load_data() |
|
220 | defaults = self.__load_data() | |
245 | defaults.update(errors.value) |
|
221 | defaults.update(errors.value) | |
246 | return htmlfill.render( |
|
222 | return htmlfill.render( | |
247 | render('admin/repos/repo_edit.html'), |
|
223 | base.render('admin/repos/repo_edit.html'), | |
248 | defaults=defaults, |
|
224 | defaults=defaults, | |
249 | errors=errors.error_dict or {}, |
|
225 | errors=errors.error_dict or {}, | |
250 | prefix_error=False, |
|
226 | prefix_error=False, | |
@@ -253,7 +229,7 b' class ReposController(BaseRepoController' | |||||
253 |
|
229 | |||
254 | except Exception: |
|
230 | except Exception: | |
255 | log.error(traceback.format_exc()) |
|
231 | log.error(traceback.format_exc()) | |
256 |
|
|
232 | webutils.flash(_('Error occurred during update of repository %s') | |
257 | % repo_name, category='error') |
|
233 | % repo_name, category='error') | |
258 | raise HTTPFound(location=url('edit_repo', repo_name=changed_name)) |
|
234 | raise HTTPFound(location=url('edit_repo', repo_name=changed_name)) | |
259 |
|
235 | |||
@@ -262,8 +238,7 b' class ReposController(BaseRepoController' | |||||
262 | repo_model = RepoModel() |
|
238 | repo_model = RepoModel() | |
263 | repo = repo_model.get_by_repo_name(repo_name) |
|
239 | repo = repo_model.get_by_repo_name(repo_name) | |
264 | if not repo: |
|
240 | if not repo: | |
265 | h.not_mapped_error(repo_name) |
|
241 | raise HTTPNotFound() | |
266 | raise HTTPFound(location=url('repos')) |
|
|||
267 | try: |
|
242 | try: | |
268 | _forks = repo.forks.count() |
|
243 | _forks = repo.forks.count() | |
269 | handle_forks = None |
|
244 | handle_forks = None | |
@@ -271,23 +246,23 b' class ReposController(BaseRepoController' | |||||
271 | do = request.POST['forks'] |
|
246 | do = request.POST['forks'] | |
272 | if do == 'detach_forks': |
|
247 | if do == 'detach_forks': | |
273 | handle_forks = 'detach' |
|
248 | handle_forks = 'detach' | |
274 |
|
|
249 | webutils.flash(_('Detached %s forks') % _forks, category='success') | |
275 | elif do == 'delete_forks': |
|
250 | elif do == 'delete_forks': | |
276 | handle_forks = 'delete' |
|
251 | handle_forks = 'delete' | |
277 |
|
|
252 | webutils.flash(_('Deleted %s forks') % _forks, category='success') | |
278 | repo_model.delete(repo, forks=handle_forks) |
|
253 | repo_model.delete(repo, forks=handle_forks) | |
279 | action_logger(request.authuser, 'admin_deleted_repo', |
|
254 | userlog.action_logger(request.authuser, 'admin_deleted_repo', | |
280 | repo_name, request.ip_addr) |
|
255 | repo_name, request.ip_addr) | |
281 | ScmModel().mark_for_invalidation(repo_name) |
|
256 | ScmModel().mark_for_invalidation(repo_name) | |
282 |
|
|
257 | webutils.flash(_('Deleted repository %s') % repo_name, category='success') | |
283 | Session().commit() |
|
258 | meta.Session().commit() | |
284 | except AttachedForksError: |
|
259 | except AttachedForksError: | |
285 |
|
|
260 | webutils.flash(_('Cannot delete repository %s which still has forks') | |
286 | % repo_name, category='warning') |
|
261 | % repo_name, category='warning') | |
287 |
|
262 | |||
288 | except Exception: |
|
263 | except Exception: | |
289 | log.error(traceback.format_exc()) |
|
264 | log.error(traceback.format_exc()) | |
290 |
|
|
265 | webutils.flash(_('An error occurred during deletion of %s') % repo_name, | |
291 | category='error') |
|
266 | category='error') | |
292 |
|
267 | |||
293 | if repo.group: |
|
268 | if repo.group: | |
@@ -297,11 +272,11 b' class ReposController(BaseRepoController' | |||||
297 | @HasRepoPermissionLevelDecorator('admin') |
|
272 | @HasRepoPermissionLevelDecorator('admin') | |
298 | def edit(self, repo_name): |
|
273 | def edit(self, repo_name): | |
299 | defaults = self.__load_data() |
|
274 | defaults = self.__load_data() | |
300 | c.repo_fields = RepositoryField.query() \ |
|
275 | c.repo_fields = db.RepositoryField.query() \ | |
301 | .filter(RepositoryField.repository == c.repo_info).all() |
|
276 | .filter(db.RepositoryField.repository == c.repo_info).all() | |
302 | c.active = 'settings' |
|
277 | c.active = 'settings' | |
303 | return htmlfill.render( |
|
278 | return htmlfill.render( | |
304 | render('admin/repos/repo_edit.html'), |
|
279 | base.render('admin/repos/repo_edit.html'), | |
305 | defaults=defaults, |
|
280 | defaults=defaults, | |
306 | encoding="UTF-8", |
|
281 | encoding="UTF-8", | |
307 | force_defaults=False) |
|
282 | force_defaults=False) | |
@@ -313,7 +288,7 b' class ReposController(BaseRepoController' | |||||
313 | defaults = RepoModel()._get_defaults(repo_name) |
|
288 | defaults = RepoModel()._get_defaults(repo_name) | |
314 |
|
289 | |||
315 | return htmlfill.render( |
|
290 | return htmlfill.render( | |
316 | render('admin/repos/repo_edit.html'), |
|
291 | base.render('admin/repos/repo_edit.html'), | |
317 | defaults=defaults, |
|
292 | defaults=defaults, | |
318 | encoding="UTF-8", |
|
293 | encoding="UTF-8", | |
319 | force_defaults=False) |
|
294 | force_defaults=False) | |
@@ -326,8 +301,8 b' class ReposController(BaseRepoController' | |||||
326 | # TODO: implement this |
|
301 | # TODO: implement this | |
327 | #action_logger(request.authuser, 'admin_changed_repo_permissions', |
|
302 | #action_logger(request.authuser, 'admin_changed_repo_permissions', | |
328 | # repo_name, request.ip_addr) |
|
303 | # repo_name, request.ip_addr) | |
329 | Session().commit() |
|
304 | meta.Session().commit() | |
330 |
|
|
305 | webutils.flash(_('Repository permissions updated'), category='success') | |
331 | raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name)) |
|
306 | raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name)) | |
332 |
|
307 | |||
333 | @HasRepoPermissionLevelDecorator('admin') |
|
308 | @HasRepoPermissionLevelDecorator('admin') | |
@@ -353,10 +328,10 b' class ReposController(BaseRepoController' | |||||
353 | # TODO: implement this |
|
328 | # TODO: implement this | |
354 | #action_logger(request.authuser, 'admin_revoked_repo_permissions', |
|
329 | #action_logger(request.authuser, 'admin_revoked_repo_permissions', | |
355 | # repo_name, request.ip_addr) |
|
330 | # repo_name, request.ip_addr) | |
356 | Session().commit() |
|
331 | meta.Session().commit() | |
357 | except Exception: |
|
332 | except Exception: | |
358 | log.error(traceback.format_exc()) |
|
333 | log.error(traceback.format_exc()) | |
359 |
|
|
334 | webutils.flash(_('An error occurred during revoking of permission'), | |
360 | category='error') |
|
335 | category='error') | |
361 | raise HTTPInternalServerError() |
|
336 | raise HTTPInternalServerError() | |
362 | return [] |
|
337 | return [] | |
@@ -364,55 +339,55 b' class ReposController(BaseRepoController' | |||||
364 | @HasRepoPermissionLevelDecorator('admin') |
|
339 | @HasRepoPermissionLevelDecorator('admin') | |
365 | def edit_fields(self, repo_name): |
|
340 | def edit_fields(self, repo_name): | |
366 | c.repo_info = self._load_repo() |
|
341 | c.repo_info = self._load_repo() | |
367 | c.repo_fields = RepositoryField.query() \ |
|
342 | c.repo_fields = db.RepositoryField.query() \ | |
368 | .filter(RepositoryField.repository == c.repo_info).all() |
|
343 | .filter(db.RepositoryField.repository == c.repo_info).all() | |
369 | c.active = 'fields' |
|
344 | c.active = 'fields' | |
370 | if request.POST: |
|
345 | if request.POST: | |
371 |
|
346 | |||
372 | raise HTTPFound(location=url('repo_edit_fields')) |
|
347 | raise HTTPFound(location=url('repo_edit_fields')) | |
373 | return render('admin/repos/repo_edit.html') |
|
348 | return base.render('admin/repos/repo_edit.html') | |
374 |
|
349 | |||
375 | @HasRepoPermissionLevelDecorator('admin') |
|
350 | @HasRepoPermissionLevelDecorator('admin') | |
376 | def create_repo_field(self, repo_name): |
|
351 | def create_repo_field(self, repo_name): | |
377 | try: |
|
352 | try: | |
378 | form_result = RepoFieldForm()().to_python(dict(request.POST)) |
|
353 | form_result = RepoFieldForm()().to_python(dict(request.POST)) | |
379 | new_field = RepositoryField() |
|
354 | new_field = db.RepositoryField() | |
380 | new_field.repository = Repository.get_by_repo_name(repo_name) |
|
355 | new_field.repository = db.Repository.get_by_repo_name(repo_name) | |
381 | new_field.field_key = form_result['new_field_key'] |
|
356 | new_field.field_key = form_result['new_field_key'] | |
382 | new_field.field_type = form_result['new_field_type'] # python type |
|
357 | new_field.field_type = form_result['new_field_type'] # python type | |
383 | new_field.field_value = form_result['new_field_value'] # set initial blank value |
|
358 | new_field.field_value = form_result['new_field_value'] # set initial blank value | |
384 | new_field.field_desc = form_result['new_field_desc'] |
|
359 | new_field.field_desc = form_result['new_field_desc'] | |
385 | new_field.field_label = form_result['new_field_label'] |
|
360 | new_field.field_label = form_result['new_field_label'] | |
386 | Session().add(new_field) |
|
361 | meta.Session().add(new_field) | |
387 | Session().commit() |
|
362 | meta.Session().commit() | |
388 | except formencode.Invalid as e: |
|
363 | except formencode.Invalid as e: | |
389 |
|
|
364 | webutils.flash(_('Field validation error: %s') % e.msg, category='error') | |
390 | except Exception as e: |
|
365 | except Exception as e: | |
391 | log.error(traceback.format_exc()) |
|
366 | log.error(traceback.format_exc()) | |
392 |
|
|
367 | webutils.flash(_('An error occurred during creation of field: %r') % e, category='error') | |
393 | raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name)) |
|
368 | raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name)) | |
394 |
|
369 | |||
395 | @HasRepoPermissionLevelDecorator('admin') |
|
370 | @HasRepoPermissionLevelDecorator('admin') | |
396 | def delete_repo_field(self, repo_name, field_id): |
|
371 | def delete_repo_field(self, repo_name, field_id): | |
397 | field = RepositoryField.get_or_404(field_id) |
|
372 | field = db.RepositoryField.get_or_404(field_id) | |
398 | try: |
|
373 | try: | |
399 | Session().delete(field) |
|
374 | meta.Session().delete(field) | |
400 | Session().commit() |
|
375 | meta.Session().commit() | |
401 | except Exception as e: |
|
376 | except Exception as e: | |
402 | log.error(traceback.format_exc()) |
|
377 | log.error(traceback.format_exc()) | |
403 | msg = _('An error occurred during removal of field') |
|
378 | msg = _('An error occurred during removal of field') | |
404 |
|
|
379 | webutils.flash(msg, category='error') | |
405 | raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name)) |
|
380 | raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name)) | |
406 |
|
381 | |||
407 | @HasRepoPermissionLevelDecorator('admin') |
|
382 | @HasRepoPermissionLevelDecorator('admin') | |
408 | def edit_advanced(self, repo_name): |
|
383 | def edit_advanced(self, repo_name): | |
409 | c.repo_info = self._load_repo() |
|
384 | c.repo_info = self._load_repo() | |
410 | c.default_user_id = kallithea.DEFAULT_USER_ID |
|
385 | c.default_user_id = kallithea.DEFAULT_USER_ID | |
411 | c.in_public_journal = UserFollowing.query() \ |
|
386 | c.in_public_journal = db.UserFollowing.query() \ | |
412 | .filter(UserFollowing.user_id == c.default_user_id) \ |
|
387 | .filter(db.UserFollowing.user_id == c.default_user_id) \ | |
413 | .filter(UserFollowing.follows_repository == c.repo_info).scalar() |
|
388 | .filter(db.UserFollowing.follows_repository == c.repo_info).scalar() | |
414 |
|
389 | |||
415 | _repos = Repository.query(sorted=True).all() |
|
390 | _repos = db.Repository.query(sorted=True).all() | |
416 | read_access_repos = RepoList(_repos, perm_level='read') |
|
391 | read_access_repos = RepoList(_repos, perm_level='read') | |
417 | c.repos_list = [(None, _('-- Not a fork --'))] |
|
392 | c.repos_list = [(None, _('-- Not a fork --'))] | |
418 | c.repos_list += [(x.repo_id, x.repo_name) |
|
393 | c.repos_list += [(x.repo_id, x.repo_name) | |
@@ -428,7 +403,7 b' class ReposController(BaseRepoController' | |||||
428 | if request.POST: |
|
403 | if request.POST: | |
429 | raise HTTPFound(location=url('repo_edit_advanced')) |
|
404 | raise HTTPFound(location=url('repo_edit_advanced')) | |
430 | return htmlfill.render( |
|
405 | return htmlfill.render( | |
431 | render('admin/repos/repo_edit.html'), |
|
406 | base.render('admin/repos/repo_edit.html'), | |
432 | defaults=defaults, |
|
407 | defaults=defaults, | |
433 | encoding="UTF-8", |
|
408 | encoding="UTF-8", | |
434 | force_defaults=False) |
|
409 | force_defaults=False) | |
@@ -443,14 +418,14 b' class ReposController(BaseRepoController' | |||||
443 | """ |
|
418 | """ | |
444 |
|
419 | |||
445 | try: |
|
420 | try: | |
446 | repo_id = Repository.get_by_repo_name(repo_name).repo_id |
|
421 | repo_id = db.Repository.get_by_repo_name(repo_name).repo_id | |
447 | user_id = kallithea.DEFAULT_USER_ID |
|
422 | user_id = kallithea.DEFAULT_USER_ID | |
448 | self.scm_model.toggle_following_repo(repo_id, user_id) |
|
423 | self.scm_model.toggle_following_repo(repo_id, user_id) | |
449 |
|
|
424 | webutils.flash(_('Updated repository visibility in public journal'), | |
450 | category='success') |
|
425 | category='success') | |
451 | Session().commit() |
|
426 | meta.Session().commit() | |
452 | except Exception: |
|
427 | except Exception: | |
453 |
|
|
428 | webutils.flash(_('An error occurred during setting this' | |
454 | ' repository in public journal'), |
|
429 | ' repository in public journal'), | |
455 | category='error') |
|
430 | category='error') | |
456 | raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name)) |
|
431 | raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name)) | |
@@ -467,15 +442,15 b' class ReposController(BaseRepoController' | |||||
467 | repo = ScmModel().mark_as_fork(repo_name, fork_id, |
|
442 | repo = ScmModel().mark_as_fork(repo_name, fork_id, | |
468 | request.authuser.username) |
|
443 | request.authuser.username) | |
469 | fork = repo.fork.repo_name if repo.fork else _('Nothing') |
|
444 | fork = repo.fork.repo_name if repo.fork else _('Nothing') | |
470 | Session().commit() |
|
445 | meta.Session().commit() | |
471 |
|
|
446 | webutils.flash(_('Marked repository %s as fork of %s') % (repo_name, fork), | |
472 | category='success') |
|
447 | category='success') | |
473 | except RepositoryError as e: |
|
448 | except RepositoryError as e: | |
474 | log.error(traceback.format_exc()) |
|
449 | log.error(traceback.format_exc()) | |
475 |
|
|
450 | webutils.flash(e, category='error') | |
476 | except Exception as e: |
|
451 | except Exception as e: | |
477 | log.error(traceback.format_exc()) |
|
452 | log.error(traceback.format_exc()) | |
478 |
|
|
453 | webutils.flash(_('An error occurred during this operation'), | |
479 | category='error') |
|
454 | category='error') | |
480 |
|
455 | |||
481 | raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name)) |
|
456 | raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name)) | |
@@ -487,13 +462,13 b' class ReposController(BaseRepoController' | |||||
487 | if request.POST: |
|
462 | if request.POST: | |
488 | try: |
|
463 | try: | |
489 | ScmModel().pull_changes(repo_name, request.authuser.username, request.ip_addr) |
|
464 | ScmModel().pull_changes(repo_name, request.authuser.username, request.ip_addr) | |
490 |
|
|
465 | webutils.flash(_('Pulled from remote location'), category='success') | |
491 | except Exception as e: |
|
466 | except Exception as e: | |
492 | log.error(traceback.format_exc()) |
|
467 | log.error(traceback.format_exc()) | |
493 |
|
|
468 | webutils.flash(_('An error occurred during pull from remote location'), | |
494 | category='error') |
|
469 | category='error') | |
495 | raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name)) |
|
470 | raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name)) | |
496 | return render('admin/repos/repo_edit.html') |
|
471 | return base.render('admin/repos/repo_edit.html') | |
497 |
|
472 | |||
498 | @HasRepoPermissionLevelDecorator('admin') |
|
473 | @HasRepoPermissionLevelDecorator('admin') | |
499 | def edit_statistics(self, repo_name): |
|
474 | def edit_statistics(self, repo_name): | |
@@ -518,11 +493,11 b' class ReposController(BaseRepoController' | |||||
518 | if request.POST: |
|
493 | if request.POST: | |
519 | try: |
|
494 | try: | |
520 | RepoModel().delete_stats(repo_name) |
|
495 | RepoModel().delete_stats(repo_name) | |
521 | Session().commit() |
|
496 | meta.Session().commit() | |
522 | except Exception as e: |
|
497 | except Exception as e: | |
523 | log.error(traceback.format_exc()) |
|
498 | log.error(traceback.format_exc()) | |
524 |
|
|
499 | webutils.flash(_('An error occurred during deletion of repository stats'), | |
525 | category='error') |
|
500 | category='error') | |
526 | raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name)) |
|
501 | raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name)) | |
527 |
|
502 | |||
528 | return render('admin/repos/repo_edit.html') |
|
503 | return base.render('admin/repos/repo_edit.html') |
@@ -35,18 +35,17 b' from tg import tmpl_context as c' | |||||
35 | from tg.i18n import ugettext as _ |
|
35 | from tg.i18n import ugettext as _ | |
36 | from webob.exc import HTTPFound |
|
36 | from webob.exc import HTTPFound | |
37 |
|
37 | |||
38 | from kallithea.config.routing import url |
|
38 | import kallithea | |
39 | from kallithea.lib import helpers as h |
|
39 | import kallithea.lib.indexers.daemon | |
|
40 | from kallithea.controllers import base | |||
|
41 | from kallithea.lib import webutils | |||
40 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired |
|
42 | from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired | |
41 | from kallithea.lib.base import BaseController, render |
|
|||
42 | from kallithea.lib.celerylib import tasks |
|
|||
43 | from kallithea.lib.exceptions import HgsubversionImportError |
|
|||
44 | from kallithea.lib.utils import repo2db_mapper, set_app_settings |
|
43 | from kallithea.lib.utils import repo2db_mapper, set_app_settings | |
45 | from kallithea.lib.utils2 import safe_str |
|
44 | from kallithea.lib.utils2 import safe_str | |
46 | from kallithea.lib.vcs import VCSError |
|
45 | from kallithea.lib.vcs import VCSError | |
47 | from kallithea.model.db import Repository, Setting, Ui |
|
46 | from kallithea.lib.webutils import url | |
|
47 | from kallithea.model import db, meta, notification | |||
48 | from kallithea.model.forms import ApplicationSettingsForm, ApplicationUiSettingsForm, ApplicationVisualisationForm |
|
48 | from kallithea.model.forms import ApplicationSettingsForm, ApplicationUiSettingsForm, ApplicationVisualisationForm | |
49 | from kallithea.model.meta import Session |
|
|||
50 | from kallithea.model.notification import EmailNotificationModel |
|
49 | from kallithea.model.notification import EmailNotificationModel | |
51 | from kallithea.model.scm import ScmModel |
|
50 | from kallithea.model.scm import ScmModel | |
52 |
|
51 | |||
@@ -54,19 +53,14 b' from kallithea.model.scm import ScmModel' | |||||
54 | log = logging.getLogger(__name__) |
|
53 | log = logging.getLogger(__name__) | |
55 |
|
54 | |||
56 |
|
55 | |||
57 | class SettingsController(BaseController): |
|
56 | class SettingsController(base.BaseController): | |
58 | """REST Controller styled on the Atom Publishing Protocol""" |
|
|||
59 | # To properly map this controller, ensure your config/routing.py |
|
|||
60 | # file has a resource setup: |
|
|||
61 | # map.resource('setting', 'settings', controller='admin/settings', |
|
|||
62 | # path_prefix='/admin', name_prefix='admin_') |
|
|||
63 |
|
57 | |||
64 | @LoginRequired(allow_default_user=True) |
|
58 | @LoginRequired(allow_default_user=True) | |
65 | def _before(self, *args, **kwargs): |
|
59 | def _before(self, *args, **kwargs): | |
66 | super(SettingsController, self)._before(*args, **kwargs) |
|
60 | super(SettingsController, self)._before(*args, **kwargs) | |
67 |
|
61 | |||
68 | def _get_hg_ui_settings(self): |
|
62 | def _get_hg_ui_settings(self): | |
69 | ret = Ui.query().all() |
|
63 | ret = db.Ui.query().all() | |
70 |
|
64 | |||
71 | settings = {} |
|
65 | settings = {} | |
72 | for each in ret: |
|
66 | for each in ret: | |
@@ -92,7 +86,7 b' class SettingsController(BaseController)' | |||||
92 | form_result = application_form.to_python(dict(request.POST)) |
|
86 | form_result = application_form.to_python(dict(request.POST)) | |
93 | except formencode.Invalid as errors: |
|
87 | except formencode.Invalid as errors: | |
94 | return htmlfill.render( |
|
88 | return htmlfill.render( | |
95 | render('admin/settings/settings.html'), |
|
89 | base.render('admin/settings/settings.html'), | |
96 | defaults=errors.value, |
|
90 | defaults=errors.value, | |
97 | errors=errors.error_dict or {}, |
|
91 | errors=errors.error_dict or {}, | |
98 | prefix_error=False, |
|
92 | prefix_error=False, | |
@@ -101,52 +95,37 b' class SettingsController(BaseController)' | |||||
101 |
|
95 | |||
102 | try: |
|
96 | try: | |
103 | if c.visual.allow_repo_location_change: |
|
97 | if c.visual.allow_repo_location_change: | |
104 | sett = Ui.get_by_key('paths', '/') |
|
98 | sett = db.Ui.get_by_key('paths', '/') | |
105 | sett.ui_value = form_result['paths_root_path'] |
|
99 | sett.ui_value = form_result['paths_root_path'] | |
106 |
|
100 | |||
107 | # HOOKS |
|
101 | # HOOKS | |
108 | sett = Ui.get_by_key('hooks', Ui.HOOK_UPDATE) |
|
102 | sett = db.Ui.get_by_key('hooks', db.Ui.HOOK_UPDATE) | |
109 | sett.ui_active = form_result['hooks_changegroup_update'] |
|
103 | sett.ui_active = form_result['hooks_changegroup_kallithea_update'] | |
110 |
|
104 | |||
111 | sett = Ui.get_by_key('hooks', Ui.HOOK_REPO_SIZE) |
|
105 | sett = db.Ui.get_by_key('hooks', db.Ui.HOOK_REPO_SIZE) | |
112 | sett.ui_active = form_result['hooks_changegroup_repo_size'] |
|
106 | sett.ui_active = form_result['hooks_changegroup_kallithea_repo_size'] | |
113 |
|
107 | |||
114 | ## EXTENSIONS |
|
108 | ## EXTENSIONS | |
115 | sett = Ui.get_or_create('extensions', 'largefiles') |
|
109 | sett = db.Ui.get_or_create('extensions', 'largefiles') | |
116 | sett.ui_active = form_result['extensions_largefiles'] |
|
110 | sett.ui_active = form_result['extensions_largefiles'] | |
117 |
|
111 | |||
118 |
sett = Ui.get_or_create('extensions', 'hg |
|
112 | # sett = db.Ui.get_or_create('extensions', 'hggit') | |
119 | sett.ui_active = form_result['extensions_hgsubversion'] |
|
|||
120 | if sett.ui_active: |
|
|||
121 | try: |
|
|||
122 | import hgsubversion # pragma: no cover |
|
|||
123 | assert hgsubversion |
|
|||
124 | except ImportError: |
|
|||
125 | raise HgsubversionImportError |
|
|||
126 |
|
||||
127 | # sett = Ui.get_or_create('extensions', 'hggit') |
|
|||
128 | # sett.ui_active = form_result['extensions_hggit'] |
|
113 | # sett.ui_active = form_result['extensions_hggit'] | |
129 |
|
114 | |||
130 | Session().commit() |
|
115 | meta.Session().commit() | |
131 |
|
||||
132 | h.flash(_('Updated VCS settings'), category='success') |
|
|||
133 |
|
116 | |||
134 | except HgsubversionImportError: |
|
117 | webutils.flash(_('Updated VCS settings'), category='success') | |
135 | log.error(traceback.format_exc()) |
|
|||
136 | h.flash(_('Unable to activate hgsubversion support. ' |
|
|||
137 | 'The "hgsubversion" library is missing'), |
|
|||
138 | category='error') |
|
|||
139 |
|
118 | |||
140 | except Exception: |
|
119 | except Exception: | |
141 | log.error(traceback.format_exc()) |
|
120 | log.error(traceback.format_exc()) | |
142 |
|
|
121 | webutils.flash(_('Error occurred while updating ' | |
143 | 'application settings'), category='error') |
|
122 | 'application settings'), category='error') | |
144 |
|
123 | |||
145 | defaults = Setting.get_app_settings() |
|
124 | defaults = db.Setting.get_app_settings() | |
146 | defaults.update(self._get_hg_ui_settings()) |
|
125 | defaults.update(self._get_hg_ui_settings()) | |
147 |
|
126 | |||
148 | return htmlfill.render( |
|
127 | return htmlfill.render( | |
149 | render('admin/settings/settings.html'), |
|
128 | base.render('admin/settings/settings.html'), | |
150 | defaults=defaults, |
|
129 | defaults=defaults, | |
151 | encoding="UTF-8", |
|
130 | encoding="UTF-8", | |
152 | force_defaults=False) |
|
131 | force_defaults=False) | |
@@ -168,33 +147,33 b' class SettingsController(BaseController)' | |||||
168 | install_git_hooks=install_git_hooks, |
|
147 | install_git_hooks=install_git_hooks, | |
169 | user=request.authuser.username, |
|
148 | user=request.authuser.username, | |
170 | overwrite_git_hooks=overwrite_git_hooks) |
|
149 | overwrite_git_hooks=overwrite_git_hooks) | |
171 |
added_msg = |
|
150 | added_msg = webutils.HTML(', ').join( | |
172 |
|
|
151 | webutils.link_to(safe_str(repo_name), webutils.url('summary_home', repo_name=repo_name)) for repo_name in added | |
173 | ) or '-' |
|
152 | ) or '-' | |
174 |
removed_msg = |
|
153 | removed_msg = webutils.HTML(', ').join( | |
175 | safe_str(repo_name) for repo_name in removed |
|
154 | safe_str(repo_name) for repo_name in removed | |
176 | ) or '-' |
|
155 | ) or '-' | |
177 |
|
|
156 | webutils.flash(webutils.HTML(_('Repositories successfully rescanned. Added: %s. Removed: %s.')) % | |
178 | (added_msg, removed_msg), category='success') |
|
157 | (added_msg, removed_msg), category='success') | |
179 |
|
158 | |||
180 | if invalidate_cache: |
|
159 | if invalidate_cache: | |
181 | log.debug('invalidating all repositories cache') |
|
160 | log.debug('invalidating all repositories cache') | |
182 | i = 0 |
|
161 | i = 0 | |
183 | for repo in Repository.query(): |
|
162 | for repo in db.Repository.query(): | |
184 | try: |
|
163 | try: | |
185 | ScmModel().mark_for_invalidation(repo.repo_name) |
|
164 | ScmModel().mark_for_invalidation(repo.repo_name) | |
186 | i += 1 |
|
165 | i += 1 | |
187 | except VCSError as e: |
|
166 | except VCSError as e: | |
188 | log.warning('VCS error invalidating %s: %s', repo.repo_name, e) |
|
167 | log.warning('VCS error invalidating %s: %s', repo.repo_name, e) | |
189 |
|
|
168 | webutils.flash(_('Invalidated %s repositories') % i, category='success') | |
190 |
|
169 | |||
191 | raise HTTPFound(location=url('admin_settings_mapping')) |
|
170 | raise HTTPFound(location=url('admin_settings_mapping')) | |
192 |
|
171 | |||
193 | defaults = Setting.get_app_settings() |
|
172 | defaults = db.Setting.get_app_settings() | |
194 | defaults.update(self._get_hg_ui_settings()) |
|
173 | defaults.update(self._get_hg_ui_settings()) | |
195 |
|
174 | |||
196 | return htmlfill.render( |
|
175 | return htmlfill.render( | |
197 | render('admin/settings/settings.html'), |
|
176 | base.render('admin/settings/settings.html'), | |
198 | defaults=defaults, |
|
177 | defaults=defaults, | |
199 | encoding="UTF-8", |
|
178 | encoding="UTF-8", | |
200 | force_defaults=False) |
|
179 | force_defaults=False) | |
@@ -208,7 +187,7 b' class SettingsController(BaseController)' | |||||
208 | form_result = application_form.to_python(dict(request.POST)) |
|
187 | form_result = application_form.to_python(dict(request.POST)) | |
209 | except formencode.Invalid as errors: |
|
188 | except formencode.Invalid as errors: | |
210 | return htmlfill.render( |
|
189 | return htmlfill.render( | |
211 | render('admin/settings/settings.html'), |
|
190 | base.render('admin/settings/settings.html'), | |
212 | defaults=errors.value, |
|
191 | defaults=errors.value, | |
213 | errors=errors.error_dict or {}, |
|
192 | errors=errors.error_dict or {}, | |
214 | prefix_error=False, |
|
193 | prefix_error=False, | |
@@ -223,25 +202,25 b' class SettingsController(BaseController)' | |||||
223 | 'captcha_public_key', |
|
202 | 'captcha_public_key', | |
224 | 'captcha_private_key', |
|
203 | 'captcha_private_key', | |
225 | ): |
|
204 | ): | |
226 | Setting.create_or_update(setting, form_result[setting]) |
|
205 | db.Setting.create_or_update(setting, form_result[setting]) | |
227 |
|
206 | |||
228 | Session().commit() |
|
207 | meta.Session().commit() | |
229 | set_app_settings(config) |
|
208 | set_app_settings(config) | |
230 |
|
|
209 | webutils.flash(_('Updated application settings'), category='success') | |
231 |
|
210 | |||
232 | except Exception: |
|
211 | except Exception: | |
233 | log.error(traceback.format_exc()) |
|
212 | log.error(traceback.format_exc()) | |
234 |
|
|
213 | webutils.flash(_('Error occurred while updating ' | |
235 | 'application settings'), |
|
214 | 'application settings'), | |
236 | category='error') |
|
215 | category='error') | |
237 |
|
216 | |||
238 | raise HTTPFound(location=url('admin_settings_global')) |
|
217 | raise HTTPFound(location=url('admin_settings_global')) | |
239 |
|
218 | |||
240 | defaults = Setting.get_app_settings() |
|
219 | defaults = db.Setting.get_app_settings() | |
241 | defaults.update(self._get_hg_ui_settings()) |
|
220 | defaults.update(self._get_hg_ui_settings()) | |
242 |
|
221 | |||
243 | return htmlfill.render( |
|
222 | return htmlfill.render( | |
244 | render('admin/settings/settings.html'), |
|
223 | base.render('admin/settings/settings.html'), | |
245 | defaults=defaults, |
|
224 | defaults=defaults, | |
246 | encoding="UTF-8", |
|
225 | encoding="UTF-8", | |
247 | force_defaults=False) |
|
226 | force_defaults=False) | |
@@ -255,7 +234,7 b' class SettingsController(BaseController)' | |||||
255 | form_result = application_form.to_python(dict(request.POST)) |
|
234 | form_result = application_form.to_python(dict(request.POST)) | |
256 | except formencode.Invalid as errors: |
|
235 | except formencode.Invalid as errors: | |
257 | return htmlfill.render( |
|
236 | return htmlfill.render( | |
258 | render('admin/settings/settings.html'), |
|
237 | base.render('admin/settings/settings.html'), | |
259 | defaults=errors.value, |
|
238 | defaults=errors.value, | |
260 | errors=errors.error_dict or {}, |
|
239 | errors=errors.error_dict or {}, | |
261 | prefix_error=False, |
|
240 | prefix_error=False, | |
@@ -277,26 +256,26 b' class SettingsController(BaseController)' | |||||
277 | ('clone_ssh_tmpl', 'clone_ssh_tmpl', 'unicode'), |
|
256 | ('clone_ssh_tmpl', 'clone_ssh_tmpl', 'unicode'), | |
278 | ] |
|
257 | ] | |
279 | for setting, form_key, type_ in settings: |
|
258 | for setting, form_key, type_ in settings: | |
280 | Setting.create_or_update(setting, form_result[form_key], type_) |
|
259 | db.Setting.create_or_update(setting, form_result[form_key], type_) | |
281 |
|
260 | |||
282 | Session().commit() |
|
261 | meta.Session().commit() | |
283 | set_app_settings(config) |
|
262 | set_app_settings(config) | |
284 |
|
|
263 | webutils.flash(_('Updated visualisation settings'), | |
285 | category='success') |
|
264 | category='success') | |
286 |
|
265 | |||
287 | except Exception: |
|
266 | except Exception: | |
288 | log.error(traceback.format_exc()) |
|
267 | log.error(traceback.format_exc()) | |
289 |
|
|
268 | webutils.flash(_('Error occurred during updating ' | |
290 | 'visualisation settings'), |
|
269 | 'visualisation settings'), | |
291 | category='error') |
|
270 | category='error') | |
292 |
|
271 | |||
293 | raise HTTPFound(location=url('admin_settings_visual')) |
|
272 | raise HTTPFound(location=url('admin_settings_visual')) | |
294 |
|
273 | |||
295 | defaults = Setting.get_app_settings() |
|
274 | defaults = db.Setting.get_app_settings() | |
296 | defaults.update(self._get_hg_ui_settings()) |
|
275 | defaults.update(self._get_hg_ui_settings()) | |
297 |
|
276 | |||
298 | return htmlfill.render( |
|
277 | return htmlfill.render( | |
299 | render('admin/settings/settings.html'), |
|
278 | base.render('admin/settings/settings.html'), | |
300 | defaults=defaults, |
|
279 | defaults=defaults, | |
301 | encoding="UTF-8", |
|
280 | encoding="UTF-8", | |
302 | force_defaults=False) |
|
281 | force_defaults=False) | |
@@ -310,7 +289,7 b' class SettingsController(BaseController)' | |||||
310 | test_body = ('Kallithea Email test, ' |
|
289 | test_body = ('Kallithea Email test, ' | |
311 | 'Kallithea version: %s' % c.kallithea_version) |
|
290 | 'Kallithea version: %s' % c.kallithea_version) | |
312 | if not test_email: |
|
291 | if not test_email: | |
313 |
|
|
292 | webutils.flash(_('Please enter email address'), category='error') | |
314 | raise HTTPFound(location=url('admin_settings_email')) |
|
293 | raise HTTPFound(location=url('admin_settings_email')) | |
315 |
|
294 | |||
316 | test_email_txt_body = EmailNotificationModel() \ |
|
295 | test_email_txt_body = EmailNotificationModel() \ | |
@@ -322,20 +301,19 b' class SettingsController(BaseController)' | |||||
322 |
|
301 | |||
323 | recipients = [test_email] if test_email else None |
|
302 | recipients = [test_email] if test_email else None | |
324 |
|
303 | |||
325 |
|
|
304 | notification.send_email(recipients, test_email_subj, | |
326 | test_email_txt_body, test_email_html_body) |
|
305 | test_email_txt_body, test_email_html_body) | |
327 |
|
306 | |||
328 |
|
|
307 | webutils.flash(_('Send email task created'), category='success') | |
329 | raise HTTPFound(location=url('admin_settings_email')) |
|
308 | raise HTTPFound(location=url('admin_settings_email')) | |
330 |
|
309 | |||
331 | defaults = Setting.get_app_settings() |
|
310 | defaults = db.Setting.get_app_settings() | |
332 | defaults.update(self._get_hg_ui_settings()) |
|
311 | defaults.update(self._get_hg_ui_settings()) | |
333 |
|
312 | |||
334 | import kallithea |
|
|||
335 | c.ini = kallithea.CONFIG |
|
313 | c.ini = kallithea.CONFIG | |
336 |
|
314 | |||
337 | return htmlfill.render( |
|
315 | return htmlfill.render( | |
338 | render('admin/settings/settings.html'), |
|
316 | base.render('admin/settings/settings.html'), | |
339 | defaults=defaults, |
|
317 | defaults=defaults, | |
340 | encoding="UTF-8", |
|
318 | encoding="UTF-8", | |
341 | force_defaults=False) |
|
319 | force_defaults=False) | |
@@ -352,16 +330,16 b' class SettingsController(BaseController)' | |||||
352 |
|
330 | |||
353 | try: |
|
331 | try: | |
354 | ui_key = ui_key and ui_key.strip() |
|
332 | ui_key = ui_key and ui_key.strip() | |
355 | if ui_key in (x.ui_key for x in Ui.get_custom_hooks()): |
|
333 | if ui_key in (x.ui_key for x in db.Ui.get_custom_hooks()): | |
356 |
|
|
334 | webutils.flash(_('Hook already exists'), category='error') | |
357 |
elif ui_key |
|
335 | elif ui_key and '.kallithea_' in ui_key: | |
358 |
|
|
336 | webutils.flash(_('Hook names with ".kallithea_" are reserved for internal use. Please use another hook name.'), category='error') | |
359 | elif ui_value and ui_key: |
|
337 | elif ui_value and ui_key: | |
360 | Ui.create_or_update_hook(ui_key, ui_value) |
|
338 | db.Ui.create_or_update_hook(ui_key, ui_value) | |
361 |
|
|
339 | webutils.flash(_('Added new hook'), category='success') | |
362 | elif hook_id: |
|
340 | elif hook_id: | |
363 | Ui.delete(hook_id) |
|
341 | db.Ui.delete(hook_id) | |
364 | Session().commit() |
|
342 | meta.Session().commit() | |
365 |
|
343 | |||
366 | # check for edits |
|
344 | # check for edits | |
367 | update = False |
|
345 | update = False | |
@@ -370,27 +348,26 b' class SettingsController(BaseController)' | |||||
370 | _d.get('hook_ui_value_new', []), |
|
348 | _d.get('hook_ui_value_new', []), | |
371 | _d.get('hook_ui_value', [])): |
|
349 | _d.get('hook_ui_value', [])): | |
372 | if v != ov: |
|
350 | if v != ov: | |
373 | Ui.create_or_update_hook(k, v) |
|
351 | db.Ui.create_or_update_hook(k, v) | |
374 | update = True |
|
352 | update = True | |
375 |
|
353 | |||
376 | if update: |
|
354 | if update: | |
377 |
|
|
355 | webutils.flash(_('Updated hooks'), category='success') | |
378 | Session().commit() |
|
356 | meta.Session().commit() | |
379 | except Exception: |
|
357 | except Exception: | |
380 | log.error(traceback.format_exc()) |
|
358 | log.error(traceback.format_exc()) | |
381 |
|
|
359 | webutils.flash(_('Error occurred during hook creation'), | |
382 | category='error') |
|
360 | category='error') | |
383 |
|
361 | |||
384 | raise HTTPFound(location=url('admin_settings_hooks')) |
|
362 | raise HTTPFound(location=url('admin_settings_hooks')) | |
385 |
|
363 | |||
386 | defaults = Setting.get_app_settings() |
|
364 | defaults = db.Setting.get_app_settings() | |
387 | defaults.update(self._get_hg_ui_settings()) |
|
365 | defaults.update(self._get_hg_ui_settings()) | |
388 |
|
366 | |||
389 |
c.hooks = Ui.get_ |
|
367 | c.custom_hooks = db.Ui.get_custom_hooks() | |
390 | c.custom_hooks = Ui.get_custom_hooks() |
|
|||
391 |
|
368 | |||
392 | return htmlfill.render( |
|
369 | return htmlfill.render( | |
393 | render('admin/settings/settings.html'), |
|
370 | base.render('admin/settings/settings.html'), | |
394 | defaults=defaults, |
|
371 | defaults=defaults, | |
395 | encoding="UTF-8", |
|
372 | encoding="UTF-8", | |
396 | force_defaults=False) |
|
373 | force_defaults=False) | |
@@ -401,15 +378,15 b' class SettingsController(BaseController)' | |||||
401 | if request.POST: |
|
378 | if request.POST: | |
402 | repo_location = self._get_hg_ui_settings()['paths_root_path'] |
|
379 | repo_location = self._get_hg_ui_settings()['paths_root_path'] | |
403 | full_index = request.POST.get('full_index', False) |
|
380 | full_index = request.POST.get('full_index', False) | |
404 |
|
|
381 | kallithea.lib.indexers.daemon.whoosh_index(repo_location, full_index) | |
405 |
|
|
382 | webutils.flash(_('Whoosh reindex task scheduled'), category='success') | |
406 | raise HTTPFound(location=url('admin_settings_search')) |
|
383 | raise HTTPFound(location=url('admin_settings_search')) | |
407 |
|
384 | |||
408 | defaults = Setting.get_app_settings() |
|
385 | defaults = db.Setting.get_app_settings() | |
409 | defaults.update(self._get_hg_ui_settings()) |
|
386 | defaults.update(self._get_hg_ui_settings()) | |
410 |
|
387 | |||
411 | return htmlfill.render( |
|
388 | return htmlfill.render( | |
412 | render('admin/settings/settings.html'), |
|
389 | base.render('admin/settings/settings.html'), | |
413 | defaults=defaults, |
|
390 | defaults=defaults, | |
414 | encoding="UTF-8", |
|
391 | encoding="UTF-8", | |
415 | force_defaults=False) |
|
392 | force_defaults=False) | |
@@ -418,17 +395,16 b' class SettingsController(BaseController)' | |||||
418 | def settings_system(self): |
|
395 | def settings_system(self): | |
419 | c.active = 'system' |
|
396 | c.active = 'system' | |
420 |
|
397 | |||
421 | defaults = Setting.get_app_settings() |
|
398 | defaults = db.Setting.get_app_settings() | |
422 | defaults.update(self._get_hg_ui_settings()) |
|
399 | defaults.update(self._get_hg_ui_settings()) | |
423 |
|
400 | |||
424 | import kallithea |
|
|||
425 | c.ini = kallithea.CONFIG |
|
401 | c.ini = kallithea.CONFIG | |
426 | server_info = Setting.get_server_info() |
|
402 | server_info = db.Setting.get_server_info() | |
427 | for key, val in server_info.items(): |
|
403 | for key, val in server_info.items(): | |
428 | setattr(c, key, val) |
|
404 | setattr(c, key, val) | |
429 |
|
405 | |||
430 | return htmlfill.render( |
|
406 | return htmlfill.render( | |
431 | render('admin/settings/settings.html'), |
|
407 | base.render('admin/settings/settings.html'), | |
432 | defaults=defaults, |
|
408 | defaults=defaults, | |
433 | encoding="UTF-8", |
|
409 | encoding="UTF-8", | |
434 | force_defaults=False) |
|
410 | force_defaults=False) |
@@ -37,16 +37,15 b' from tg import tmpl_context as c' | |||||
37 | from tg.i18n import ugettext as _ |
|
37 | from tg.i18n import ugettext as _ | |
38 | from webob.exc import HTTPFound, HTTPInternalServerError |
|
38 | from webob.exc import HTTPFound, HTTPInternalServerError | |
39 |
|
39 | |||
40 | from kallithea.config.routing import url |
|
40 | import kallithea.lib.helpers as h | |
41 |
from kallithea. |
|
41 | from kallithea.controllers import base | |
|
42 | from kallithea.lib import webutils | |||
42 | from kallithea.lib.auth import HasPermissionAnyDecorator, HasUserGroupPermissionLevelDecorator, LoginRequired |
|
43 | from kallithea.lib.auth import HasPermissionAnyDecorator, HasUserGroupPermissionLevelDecorator, LoginRequired | |
43 | from kallithea.lib.base import BaseController, render |
|
|||
44 | from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException |
|
44 | from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException | |
45 | from kallithea.lib.utils import action_logger |
|
|||
46 | from kallithea.lib.utils2 import safe_int, safe_str |
|
45 | from kallithea.lib.utils2 import safe_int, safe_str | |
47 | from kallithea.model.db import User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserGroupToPerm |
|
46 | from kallithea.lib.webutils import url | |
|
47 | from kallithea.model import db, meta, userlog | |||
48 | from kallithea.model.forms import CustomDefaultPermissionsForm, UserGroupForm, UserGroupPermsForm |
|
48 | from kallithea.model.forms import CustomDefaultPermissionsForm, UserGroupForm, UserGroupPermsForm | |
49 | from kallithea.model.meta import Session |
|
|||
50 | from kallithea.model.scm import UserGroupList |
|
49 | from kallithea.model.scm import UserGroupList | |
51 | from kallithea.model.user_group import UserGroupModel |
|
50 | from kallithea.model.user_group import UserGroupModel | |
52 |
|
51 | |||
@@ -54,8 +53,7 b' from kallithea.model.user_group import U' | |||||
54 | log = logging.getLogger(__name__) |
|
53 | log = logging.getLogger(__name__) | |
55 |
|
54 | |||
56 |
|
55 | |||
57 | class UserGroupsController(BaseController): |
|
56 | class UserGroupsController(base.BaseController): | |
58 | """REST Controller styled on the Atom Publishing Protocol""" |
|
|||
59 |
|
57 | |||
60 | @LoginRequired(allow_default_user=True) |
|
58 | @LoginRequired(allow_default_user=True) | |
61 | def _before(self, *args, **kwargs): |
|
59 | def _before(self, *args, **kwargs): | |
@@ -67,7 +65,7 b' class UserGroupsController(BaseControlle' | |||||
67 |
|
65 | |||
68 | c.group_members = [(x.user_id, x.username) for x in c.group_members_obj] |
|
66 | c.group_members = [(x.user_id, x.username) for x in c.group_members_obj] | |
69 | c.available_members = sorted(((x.user_id, x.username) for x in |
|
67 | c.available_members = sorted(((x.user_id, x.username) for x in | |
70 | User.query().all()), |
|
68 | db.User.query().all()), | |
71 | key=lambda u: u[1].lower()) |
|
69 | key=lambda u: u[1].lower()) | |
72 |
|
70 | |||
73 | def __load_defaults(self, user_group_id): |
|
71 | def __load_defaults(self, user_group_id): | |
@@ -76,13 +74,13 b' class UserGroupsController(BaseControlle' | |||||
76 |
|
74 | |||
77 | :param user_group_id: |
|
75 | :param user_group_id: | |
78 | """ |
|
76 | """ | |
79 | user_group = UserGroup.get_or_404(user_group_id) |
|
77 | user_group = db.UserGroup.get_or_404(user_group_id) | |
80 | data = user_group.get_dict() |
|
78 | data = user_group.get_dict() | |
81 | return data |
|
79 | return data | |
82 |
|
80 | |||
83 | def index(self, format='html'): |
|
81 | def index(self, format='html'): | |
84 | _list = UserGroup.query() \ |
|
82 | _list = db.UserGroup.query() \ | |
85 | .order_by(func.lower(UserGroup.users_group_name)) \ |
|
83 | .order_by(func.lower(db.UserGroup.users_group_name)) \ | |
86 | .all() |
|
84 | .all() | |
87 | group_iter = UserGroupList(_list, perm_level='admin') |
|
85 | group_iter = UserGroupList(_list, perm_level='admin') | |
88 | user_groups_data = [] |
|
86 | user_groups_data = [] | |
@@ -91,21 +89,21 b' class UserGroupsController(BaseControlle' | |||||
91 |
|
89 | |||
92 | def user_group_name(user_group_id, user_group_name): |
|
90 | def user_group_name(user_group_id, user_group_name): | |
93 | return template.get_def("user_group_name") \ |
|
91 | return template.get_def("user_group_name") \ | |
94 |
.render_unicode(user_group_id, user_group_name, _=_, |
|
92 | .render_unicode(user_group_id, user_group_name, _=_, webutils=webutils, c=c) | |
95 |
|
93 | |||
96 | def user_group_actions(user_group_id, user_group_name): |
|
94 | def user_group_actions(user_group_id, user_group_name): | |
97 | return template.get_def("user_group_actions") \ |
|
95 | return template.get_def("user_group_actions") \ | |
98 |
.render_unicode(user_group_id, user_group_name, _=_, |
|
96 | .render_unicode(user_group_id, user_group_name, _=_, webutils=webutils, c=c) | |
99 |
|
97 | |||
100 | for user_gr in group_iter: |
|
98 | for user_gr in group_iter: | |
101 | user_groups_data.append({ |
|
99 | user_groups_data.append({ | |
102 | "raw_name": user_gr.users_group_name, |
|
100 | "raw_name": user_gr.users_group_name, | |
103 | "group_name": user_group_name(user_gr.users_group_id, |
|
101 | "group_name": user_group_name(user_gr.users_group_id, | |
104 | user_gr.users_group_name), |
|
102 | user_gr.users_group_name), | |
105 |
"desc": |
|
103 | "desc": webutils.escape(user_gr.user_group_description), | |
106 | "members": len(user_gr.members), |
|
104 | "members": len(user_gr.members), | |
107 | "active": h.boolicon(user_gr.users_group_active), |
|
105 | "active": h.boolicon(user_gr.users_group_active), | |
108 |
"owner": |
|
106 | "owner": user_gr.owner.username, | |
109 | "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name) |
|
107 | "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name) | |
110 | }) |
|
108 | }) | |
111 |
|
109 | |||
@@ -115,7 +113,7 b' class UserGroupsController(BaseControlle' | |||||
115 | "records": user_groups_data |
|
113 | "records": user_groups_data | |
116 | } |
|
114 | } | |
117 |
|
115 | |||
118 | return render('admin/user_groups/user_groups.html') |
|
116 | return base.render('admin/user_groups/user_groups.html') | |
119 |
|
117 | |||
120 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
118 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') | |
121 | def create(self): |
|
119 | def create(self): | |
@@ -128,15 +126,15 b' class UserGroupsController(BaseControlle' | |||||
128 | active=form_result['users_group_active']) |
|
126 | active=form_result['users_group_active']) | |
129 |
|
127 | |||
130 | gr = form_result['users_group_name'] |
|
128 | gr = form_result['users_group_name'] | |
131 | action_logger(request.authuser, |
|
129 | userlog.action_logger(request.authuser, | |
132 | 'admin_created_users_group:%s' % gr, |
|
130 | 'admin_created_users_group:%s' % gr, | |
133 | None, request.ip_addr) |
|
131 | None, request.ip_addr) | |
134 |
|
|
132 | webutils.flash(webutils.HTML(_('Created user group %s')) % webutils.link_to(gr, url('edit_users_group', id=ug.users_group_id)), | |
135 | category='success') |
|
133 | category='success') | |
136 | Session().commit() |
|
134 | meta.Session().commit() | |
137 | except formencode.Invalid as errors: |
|
135 | except formencode.Invalid as errors: | |
138 | return htmlfill.render( |
|
136 | return htmlfill.render( | |
139 | render('admin/user_groups/user_group_add.html'), |
|
137 | base.render('admin/user_groups/user_group_add.html'), | |
140 | defaults=errors.value, |
|
138 | defaults=errors.value, | |
141 | errors=errors.error_dict or {}, |
|
139 | errors=errors.error_dict or {}, | |
142 | prefix_error=False, |
|
140 | prefix_error=False, | |
@@ -144,18 +142,18 b' class UserGroupsController(BaseControlle' | |||||
144 | force_defaults=False) |
|
142 | force_defaults=False) | |
145 | except Exception: |
|
143 | except Exception: | |
146 | log.error(traceback.format_exc()) |
|
144 | log.error(traceback.format_exc()) | |
147 |
|
|
145 | webutils.flash(_('Error occurred during creation of user group %s') | |
148 | % request.POST.get('users_group_name'), category='error') |
|
146 | % request.POST.get('users_group_name'), category='error') | |
149 |
|
147 | |||
150 | raise HTTPFound(location=url('users_groups')) |
|
148 | raise HTTPFound(location=url('users_groups')) | |
151 |
|
149 | |||
152 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
150 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') | |
153 | def new(self, format='html'): |
|
151 | def new(self, format='html'): | |
154 | return render('admin/user_groups/user_group_add.html') |
|
152 | return base.render('admin/user_groups/user_group_add.html') | |
155 |
|
153 | |||
156 | @HasUserGroupPermissionLevelDecorator('admin') |
|
154 | @HasUserGroupPermissionLevelDecorator('admin') | |
157 | def update(self, id): |
|
155 | def update(self, id): | |
158 | c.user_group = UserGroup.get_or_404(id) |
|
156 | c.user_group = db.UserGroup.get_or_404(id) | |
159 | c.active = 'settings' |
|
157 | c.active = 'settings' | |
160 | self.__load_data(id) |
|
158 | self.__load_data(id) | |
161 |
|
159 | |||
@@ -169,11 +167,11 b' class UserGroupsController(BaseControlle' | |||||
169 | form_result = users_group_form.to_python(request.POST) |
|
167 | form_result = users_group_form.to_python(request.POST) | |
170 | UserGroupModel().update(c.user_group, form_result) |
|
168 | UserGroupModel().update(c.user_group, form_result) | |
171 | gr = form_result['users_group_name'] |
|
169 | gr = form_result['users_group_name'] | |
172 | action_logger(request.authuser, |
|
170 | userlog.action_logger(request.authuser, | |
173 | 'admin_updated_users_group:%s' % gr, |
|
171 | 'admin_updated_users_group:%s' % gr, | |
174 | None, request.ip_addr) |
|
172 | None, request.ip_addr) | |
175 |
|
|
173 | webutils.flash(_('Updated user group %s') % gr, category='success') | |
176 | Session().commit() |
|
174 | meta.Session().commit() | |
177 | except formencode.Invalid as errors: |
|
175 | except formencode.Invalid as errors: | |
178 | ug_model = UserGroupModel() |
|
176 | ug_model = UserGroupModel() | |
179 | defaults = errors.value |
|
177 | defaults = errors.value | |
@@ -186,7 +184,7 b' class UserGroupsController(BaseControlle' | |||||
186 | }) |
|
184 | }) | |
187 |
|
185 | |||
188 | return htmlfill.render( |
|
186 | return htmlfill.render( | |
189 | render('admin/user_groups/user_group_edit.html'), |
|
187 | base.render('admin/user_groups/user_group_edit.html'), | |
190 | defaults=defaults, |
|
188 | defaults=defaults, | |
191 | errors=e, |
|
189 | errors=e, | |
192 | prefix_error=False, |
|
190 | prefix_error=False, | |
@@ -194,36 +192,36 b' class UserGroupsController(BaseControlle' | |||||
194 | force_defaults=False) |
|
192 | force_defaults=False) | |
195 | except Exception: |
|
193 | except Exception: | |
196 | log.error(traceback.format_exc()) |
|
194 | log.error(traceback.format_exc()) | |
197 |
|
|
195 | webutils.flash(_('Error occurred during update of user group %s') | |
198 | % request.POST.get('users_group_name'), category='error') |
|
196 | % request.POST.get('users_group_name'), category='error') | |
199 |
|
197 | |||
200 | raise HTTPFound(location=url('edit_users_group', id=id)) |
|
198 | raise HTTPFound(location=url('edit_users_group', id=id)) | |
201 |
|
199 | |||
202 | @HasUserGroupPermissionLevelDecorator('admin') |
|
200 | @HasUserGroupPermissionLevelDecorator('admin') | |
203 | def delete(self, id): |
|
201 | def delete(self, id): | |
204 | usr_gr = UserGroup.get_or_404(id) |
|
202 | usr_gr = db.UserGroup.get_or_404(id) | |
205 | try: |
|
203 | try: | |
206 | UserGroupModel().delete(usr_gr) |
|
204 | UserGroupModel().delete(usr_gr) | |
207 | Session().commit() |
|
205 | meta.Session().commit() | |
208 |
|
|
206 | webutils.flash(_('Successfully deleted user group'), category='success') | |
209 | except UserGroupsAssignedException as e: |
|
207 | except UserGroupsAssignedException as e: | |
210 |
|
|
208 | webutils.flash(e, category='error') | |
211 | except Exception: |
|
209 | except Exception: | |
212 | log.error(traceback.format_exc()) |
|
210 | log.error(traceback.format_exc()) | |
213 |
|
|
211 | webutils.flash(_('An error occurred during deletion of user group'), | |
214 | category='error') |
|
212 | category='error') | |
215 | raise HTTPFound(location=url('users_groups')) |
|
213 | raise HTTPFound(location=url('users_groups')) | |
216 |
|
214 | |||
217 | @HasUserGroupPermissionLevelDecorator('admin') |
|
215 | @HasUserGroupPermissionLevelDecorator('admin') | |
218 | def edit(self, id, format='html'): |
|
216 | def edit(self, id, format='html'): | |
219 | c.user_group = UserGroup.get_or_404(id) |
|
217 | c.user_group = db.UserGroup.get_or_404(id) | |
220 | c.active = 'settings' |
|
218 | c.active = 'settings' | |
221 | self.__load_data(id) |
|
219 | self.__load_data(id) | |
222 |
|
220 | |||
223 | defaults = self.__load_defaults(id) |
|
221 | defaults = self.__load_defaults(id) | |
224 |
|
222 | |||
225 | return htmlfill.render( |
|
223 | return htmlfill.render( | |
226 | render('admin/user_groups/user_group_edit.html'), |
|
224 | base.render('admin/user_groups/user_group_edit.html'), | |
227 | defaults=defaults, |
|
225 | defaults=defaults, | |
228 | encoding="UTF-8", |
|
226 | encoding="UTF-8", | |
229 | force_defaults=False |
|
227 | force_defaults=False | |
@@ -231,7 +229,7 b' class UserGroupsController(BaseControlle' | |||||
231 |
|
229 | |||
232 | @HasUserGroupPermissionLevelDecorator('admin') |
|
230 | @HasUserGroupPermissionLevelDecorator('admin') | |
233 | def edit_perms(self, id): |
|
231 | def edit_perms(self, id): | |
234 | c.user_group = UserGroup.get_or_404(id) |
|
232 | c.user_group = db.UserGroup.get_or_404(id) | |
235 | c.active = 'perms' |
|
233 | c.active = 'perms' | |
236 |
|
234 | |||
237 | defaults = {} |
|
235 | defaults = {} | |
@@ -245,7 +243,7 b' class UserGroupsController(BaseControlle' | |||||
245 | p.permission.permission_name}) |
|
243 | p.permission.permission_name}) | |
246 |
|
244 | |||
247 | return htmlfill.render( |
|
245 | return htmlfill.render( | |
248 | render('admin/user_groups/user_group_edit.html'), |
|
246 | base.render('admin/user_groups/user_group_edit.html'), | |
249 | defaults=defaults, |
|
247 | defaults=defaults, | |
250 | encoding="UTF-8", |
|
248 | encoding="UTF-8", | |
251 | force_defaults=False |
|
249 | force_defaults=False | |
@@ -258,7 +256,7 b' class UserGroupsController(BaseControlle' | |||||
258 |
|
256 | |||
259 | :param id: |
|
257 | :param id: | |
260 | """ |
|
258 | """ | |
261 | user_group = UserGroup.get_or_404(id) |
|
259 | user_group = db.UserGroup.get_or_404(id) | |
262 | form = UserGroupPermsForm()().to_python(request.POST) |
|
260 | form = UserGroupPermsForm()().to_python(request.POST) | |
263 |
|
261 | |||
264 | # set the permissions ! |
|
262 | # set the permissions ! | |
@@ -266,13 +264,13 b' class UserGroupsController(BaseControlle' | |||||
266 | UserGroupModel()._update_permissions(user_group, form['perms_new'], |
|
264 | UserGroupModel()._update_permissions(user_group, form['perms_new'], | |
267 | form['perms_updates']) |
|
265 | form['perms_updates']) | |
268 | except RepoGroupAssignmentError: |
|
266 | except RepoGroupAssignmentError: | |
269 |
|
|
267 | webutils.flash(_('Target group cannot be the same'), category='error') | |
270 | raise HTTPFound(location=url('edit_user_group_perms', id=id)) |
|
268 | raise HTTPFound(location=url('edit_user_group_perms', id=id)) | |
271 | # TODO: implement this |
|
269 | # TODO: implement this | |
272 | #action_logger(request.authuser, 'admin_changed_repo_permissions', |
|
270 | #action_logger(request.authuser, 'admin_changed_repo_permissions', | |
273 | # repo_name, request.ip_addr) |
|
271 | # repo_name, request.ip_addr) | |
274 | Session().commit() |
|
272 | meta.Session().commit() | |
275 |
|
|
273 | webutils.flash(_('User group permissions updated'), category='success') | |
276 | raise HTTPFound(location=url('edit_user_group_perms', id=id)) |
|
274 | raise HTTPFound(location=url('edit_user_group_perms', id=id)) | |
277 |
|
275 | |||
278 | @HasUserGroupPermissionLevelDecorator('admin') |
|
276 | @HasUserGroupPermissionLevelDecorator('admin') | |
@@ -288,7 +286,7 b' class UserGroupsController(BaseControlle' | |||||
288 | if not request.authuser.is_admin: |
|
286 | if not request.authuser.is_admin: | |
289 | if obj_type == 'user' and request.authuser.user_id == obj_id: |
|
287 | if obj_type == 'user' and request.authuser.user_id == obj_id: | |
290 | msg = _('Cannot revoke permission for yourself as admin') |
|
288 | msg = _('Cannot revoke permission for yourself as admin') | |
291 |
|
|
289 | webutils.flash(msg, category='warning') | |
292 | raise Exception('revoke admin permission on self') |
|
290 | raise Exception('revoke admin permission on self') | |
293 | if obj_type == 'user': |
|
291 | if obj_type == 'user': | |
294 | UserGroupModel().revoke_user_permission(user_group=id, |
|
292 | UserGroupModel().revoke_user_permission(user_group=id, | |
@@ -296,36 +294,36 b' class UserGroupsController(BaseControlle' | |||||
296 | elif obj_type == 'user_group': |
|
294 | elif obj_type == 'user_group': | |
297 | UserGroupModel().revoke_user_group_permission(target_user_group=id, |
|
295 | UserGroupModel().revoke_user_group_permission(target_user_group=id, | |
298 | user_group=obj_id) |
|
296 | user_group=obj_id) | |
299 | Session().commit() |
|
297 | meta.Session().commit() | |
300 | except Exception: |
|
298 | except Exception: | |
301 | log.error(traceback.format_exc()) |
|
299 | log.error(traceback.format_exc()) | |
302 |
|
|
300 | webutils.flash(_('An error occurred during revoking of permission'), | |
303 | category='error') |
|
301 | category='error') | |
304 | raise HTTPInternalServerError() |
|
302 | raise HTTPInternalServerError() | |
305 |
|
303 | |||
306 | @HasUserGroupPermissionLevelDecorator('admin') |
|
304 | @HasUserGroupPermissionLevelDecorator('admin') | |
307 | def edit_default_perms(self, id): |
|
305 | def edit_default_perms(self, id): | |
308 | c.user_group = UserGroup.get_or_404(id) |
|
306 | c.user_group = db.UserGroup.get_or_404(id) | |
309 | c.active = 'default_perms' |
|
307 | c.active = 'default_perms' | |
310 |
|
308 | |||
311 | permissions = { |
|
309 | permissions = { | |
312 | 'repositories': {}, |
|
310 | 'repositories': {}, | |
313 | 'repositories_groups': {} |
|
311 | 'repositories_groups': {} | |
314 | } |
|
312 | } | |
315 | ugroup_repo_perms = UserGroupRepoToPerm.query() \ |
|
313 | ugroup_repo_perms = db.UserGroupRepoToPerm.query() \ | |
316 | .options(joinedload(UserGroupRepoToPerm.permission)) \ |
|
314 | .options(joinedload(db.UserGroupRepoToPerm.permission)) \ | |
317 | .options(joinedload(UserGroupRepoToPerm.repository)) \ |
|
315 | .options(joinedload(db.UserGroupRepoToPerm.repository)) \ | |
318 | .filter(UserGroupRepoToPerm.users_group_id == id) \ |
|
316 | .filter(db.UserGroupRepoToPerm.users_group_id == id) \ | |
319 | .all() |
|
317 | .all() | |
320 |
|
318 | |||
321 | for gr in ugroup_repo_perms: |
|
319 | for gr in ugroup_repo_perms: | |
322 | permissions['repositories'][gr.repository.repo_name] \ |
|
320 | permissions['repositories'][gr.repository.repo_name] \ | |
323 | = gr.permission.permission_name |
|
321 | = gr.permission.permission_name | |
324 |
|
322 | |||
325 | ugroup_group_perms = UserGroupRepoGroupToPerm.query() \ |
|
323 | ugroup_group_perms = db.UserGroupRepoGroupToPerm.query() \ | |
326 | .options(joinedload(UserGroupRepoGroupToPerm.permission)) \ |
|
324 | .options(joinedload(db.UserGroupRepoGroupToPerm.permission)) \ | |
327 | .options(joinedload(UserGroupRepoGroupToPerm.group)) \ |
|
325 | .options(joinedload(db.UserGroupRepoGroupToPerm.group)) \ | |
328 | .filter(UserGroupRepoGroupToPerm.users_group_id == id) \ |
|
326 | .filter(db.UserGroupRepoGroupToPerm.users_group_id == id) \ | |
329 | .all() |
|
327 | .all() | |
330 |
|
328 | |||
331 | for gr in ugroup_group_perms: |
|
329 | for gr in ugroup_group_perms: | |
@@ -346,7 +344,7 b' class UserGroupsController(BaseControlle' | |||||
346 | }) |
|
344 | }) | |
347 |
|
345 | |||
348 | return htmlfill.render( |
|
346 | return htmlfill.render( | |
349 | render('admin/user_groups/user_group_edit.html'), |
|
347 | base.render('admin/user_groups/user_group_edit.html'), | |
350 | defaults=defaults, |
|
348 | defaults=defaults, | |
351 | encoding="UTF-8", |
|
349 | encoding="UTF-8", | |
352 | force_defaults=False |
|
350 | force_defaults=False | |
@@ -354,7 +352,7 b' class UserGroupsController(BaseControlle' | |||||
354 |
|
352 | |||
355 | @HasUserGroupPermissionLevelDecorator('admin') |
|
353 | @HasUserGroupPermissionLevelDecorator('admin') | |
356 | def update_default_perms(self, id): |
|
354 | def update_default_perms(self, id): | |
357 | user_group = UserGroup.get_or_404(id) |
|
355 | user_group = db.UserGroup.get_or_404(id) | |
358 |
|
356 | |||
359 | try: |
|
357 | try: | |
360 | form = CustomDefaultPermissionsForm()() |
|
358 | form = CustomDefaultPermissionsForm()() | |
@@ -362,11 +360,11 b' class UserGroupsController(BaseControlle' | |||||
362 |
|
360 | |||
363 | usergroup_model = UserGroupModel() |
|
361 | usergroup_model = UserGroupModel() | |
364 |
|
362 | |||
365 | defs = UserGroupToPerm.query() \ |
|
363 | defs = db.UserGroupToPerm.query() \ | |
366 | .filter(UserGroupToPerm.users_group == user_group) \ |
|
364 | .filter(db.UserGroupToPerm.users_group == user_group) \ | |
367 | .all() |
|
365 | .all() | |
368 | for ug in defs: |
|
366 | for ug in defs: | |
369 | Session().delete(ug) |
|
367 | meta.Session().delete(ug) | |
370 |
|
368 | |||
371 | if form_result['create_repo_perm']: |
|
369 | if form_result['create_repo_perm']: | |
372 | usergroup_model.grant_perm(id, 'hg.create.repository') |
|
370 | usergroup_model.grant_perm(id, 'hg.create.repository') | |
@@ -381,29 +379,29 b' class UserGroupsController(BaseControlle' | |||||
381 | else: |
|
379 | else: | |
382 | usergroup_model.grant_perm(id, 'hg.fork.none') |
|
380 | usergroup_model.grant_perm(id, 'hg.fork.none') | |
383 |
|
381 | |||
384 |
|
|
382 | webutils.flash(_("Updated permissions"), category='success') | |
385 | Session().commit() |
|
383 | meta.Session().commit() | |
386 | except Exception: |
|
384 | except Exception: | |
387 | log.error(traceback.format_exc()) |
|
385 | log.error(traceback.format_exc()) | |
388 |
|
|
386 | webutils.flash(_('An error occurred during permissions saving'), | |
389 | category='error') |
|
387 | category='error') | |
390 |
|
388 | |||
391 | raise HTTPFound(location=url('edit_user_group_default_perms', id=id)) |
|
389 | raise HTTPFound(location=url('edit_user_group_default_perms', id=id)) | |
392 |
|
390 | |||
393 | @HasUserGroupPermissionLevelDecorator('admin') |
|
391 | @HasUserGroupPermissionLevelDecorator('admin') | |
394 | def edit_advanced(self, id): |
|
392 | def edit_advanced(self, id): | |
395 | c.user_group = UserGroup.get_or_404(id) |
|
393 | c.user_group = db.UserGroup.get_or_404(id) | |
396 | c.active = 'advanced' |
|
394 | c.active = 'advanced' | |
397 | c.group_members_obj = sorted((x.user for x in c.user_group.members), |
|
395 | c.group_members_obj = sorted((x.user for x in c.user_group.members), | |
398 | key=lambda u: u.username.lower()) |
|
396 | key=lambda u: u.username.lower()) | |
399 | return render('admin/user_groups/user_group_edit.html') |
|
397 | return base.render('admin/user_groups/user_group_edit.html') | |
400 |
|
398 | |||
401 | @HasUserGroupPermissionLevelDecorator('admin') |
|
399 | @HasUserGroupPermissionLevelDecorator('admin') | |
402 | def edit_members(self, id): |
|
400 | def edit_members(self, id): | |
403 | c.user_group = UserGroup.get_or_404(id) |
|
401 | c.user_group = db.UserGroup.get_or_404(id) | |
404 | c.active = 'members' |
|
402 | c.active = 'members' | |
405 | c.group_members_obj = sorted((x.user for x in c.user_group.members), |
|
403 | c.group_members_obj = sorted((x.user for x in c.user_group.members), | |
406 | key=lambda u: u.username.lower()) |
|
404 | key=lambda u: u.username.lower()) | |
407 |
|
405 | |||
408 | c.group_members = [(x.user_id, x.username) for x in c.group_members_obj] |
|
406 | c.group_members = [(x.user_id, x.username) for x in c.group_members_obj] | |
409 | return render('admin/user_groups/user_group_edit.html') |
|
407 | return base.render('admin/user_groups/user_group_edit.html') |
@@ -37,18 +37,16 b' from tg.i18n import ugettext as _' | |||||
37 | from webob.exc import HTTPFound, HTTPNotFound |
|
37 | from webob.exc import HTTPFound, HTTPNotFound | |
38 |
|
38 | |||
39 | import kallithea |
|
39 | import kallithea | |
40 | from kallithea.config.routing import url |
|
40 | import kallithea.lib.helpers as h | |
41 |
from kallithea. |
|
41 | from kallithea.controllers import base | |
42 |
from kallithea.lib import |
|
42 | from kallithea.lib import auth_modules, webutils | |
43 | from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired |
|
43 | from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator, LoginRequired | |
44 | from kallithea.lib.base import BaseController, IfSshEnabled, render |
|
|||
45 | from kallithea.lib.exceptions import DefaultUserException, UserCreationError, UserOwnsReposException |
|
44 | from kallithea.lib.exceptions import DefaultUserException, UserCreationError, UserOwnsReposException | |
46 | from kallithea.lib.utils import action_logger |
|
|||
47 | from kallithea.lib.utils2 import datetime_to_time, generate_api_key, safe_int |
|
45 | from kallithea.lib.utils2 import datetime_to_time, generate_api_key, safe_int | |
|
46 | from kallithea.lib.webutils import fmt_date, url | |||
|
47 | from kallithea.model import db, meta, userlog | |||
48 | from kallithea.model.api_key import ApiKeyModel |
|
48 | from kallithea.model.api_key import ApiKeyModel | |
49 | from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm |
|
|||
50 | from kallithea.model.forms import CustomDefaultPermissionsForm, UserForm |
|
49 | from kallithea.model.forms import CustomDefaultPermissionsForm, UserForm | |
51 | from kallithea.model.meta import Session |
|
|||
52 | from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException |
|
50 | from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException | |
53 | from kallithea.model.user import UserModel |
|
51 | from kallithea.model.user import UserModel | |
54 |
|
52 | |||
@@ -56,8 +54,7 b' from kallithea.model.user import UserMod' | |||||
56 | log = logging.getLogger(__name__) |
|
54 | log = logging.getLogger(__name__) | |
57 |
|
55 | |||
58 |
|
56 | |||
59 | class UsersController(BaseController): |
|
57 | class UsersController(base.BaseController): | |
60 | """REST Controller styled on the Atom Publishing Protocol""" |
|
|||
61 |
|
58 | |||
62 | @LoginRequired() |
|
59 | @LoginRequired() | |
63 | @HasPermissionAnyDecorator('hg.admin') |
|
60 | @HasPermissionAnyDecorator('hg.admin') | |
@@ -65,9 +62,9 b' class UsersController(BaseController):' | |||||
65 | super(UsersController, self)._before(*args, **kwargs) |
|
62 | super(UsersController, self)._before(*args, **kwargs) | |
66 |
|
63 | |||
67 | def index(self, format='html'): |
|
64 | def index(self, format='html'): | |
68 | c.users_list = User.query().order_by(User.username) \ |
|
65 | c.users_list = db.User.query().order_by(db.User.username) \ | |
69 | .filter_by(is_default_user=False) \ |
|
66 | .filter_by(is_default_user=False) \ | |
70 | .order_by(func.lower(User.username)) \ |
|
67 | .order_by(func.lower(db.User.username)) \ | |
71 | .all() |
|
68 | .all() | |
72 |
|
69 | |||
73 | users_data = [] |
|
70 | users_data = [] | |
@@ -78,20 +75,20 b' class UsersController(BaseController):' | |||||
78 |
|
75 | |||
79 | def username(user_id, username): |
|
76 | def username(user_id, username): | |
80 | return template.get_def("user_name") \ |
|
77 | return template.get_def("user_name") \ | |
81 |
.render_unicode(user_id, username, _=_, |
|
78 | .render_unicode(user_id, username, _=_, webutils=webutils, c=c) | |
82 |
|
79 | |||
83 | def user_actions(user_id, username): |
|
80 | def user_actions(user_id, username): | |
84 | return template.get_def("user_actions") \ |
|
81 | return template.get_def("user_actions") \ | |
85 |
.render_unicode(user_id, username, _=_, |
|
82 | .render_unicode(user_id, username, _=_, webutils=webutils, c=c) | |
86 |
|
83 | |||
87 | for user in c.users_list: |
|
84 | for user in c.users_list: | |
88 | users_data.append({ |
|
85 | users_data.append({ | |
89 | "gravatar": grav_tmpl % h.gravatar(user.email, size=20), |
|
86 | "gravatar": grav_tmpl % h.gravatar(user.email, size=20), | |
90 | "raw_name": user.username, |
|
87 | "raw_name": user.username, | |
91 | "username": username(user.user_id, user.username), |
|
88 | "username": username(user.user_id, user.username), | |
92 |
"firstname": |
|
89 | "firstname": webutils.escape(user.name), | |
93 |
"lastname": |
|
90 | "lastname": webutils.escape(user.lastname), | |
94 |
"last_login": |
|
91 | "last_login": fmt_date(user.last_login), | |
95 | "last_login_raw": datetime_to_time(user.last_login), |
|
92 | "last_login_raw": datetime_to_time(user.last_login), | |
96 | "active": h.boolicon(user.active), |
|
93 | "active": h.boolicon(user.active), | |
97 | "admin": h.boolicon(user.admin), |
|
94 | "admin": h.boolicon(user.admin), | |
@@ -106,41 +103,41 b' class UsersController(BaseController):' | |||||
106 | "records": users_data |
|
103 | "records": users_data | |
107 | } |
|
104 | } | |
108 |
|
105 | |||
109 | return render('admin/users/users.html') |
|
106 | return base.render('admin/users/users.html') | |
110 |
|
107 | |||
111 | def create(self): |
|
108 | def create(self): | |
112 | c.default_extern_type = User.DEFAULT_AUTH_TYPE |
|
109 | c.default_extern_type = db.User.DEFAULT_AUTH_TYPE | |
113 | c.default_extern_name = '' |
|
110 | c.default_extern_name = '' | |
114 | user_model = UserModel() |
|
111 | user_model = UserModel() | |
115 | user_form = UserForm()() |
|
112 | user_form = UserForm()() | |
116 | try: |
|
113 | try: | |
117 | form_result = user_form.to_python(dict(request.POST)) |
|
114 | form_result = user_form.to_python(dict(request.POST)) | |
118 | user = user_model.create(form_result) |
|
115 | user = user_model.create(form_result) | |
119 | action_logger(request.authuser, 'admin_created_user:%s' % user.username, |
|
116 | userlog.action_logger(request.authuser, 'admin_created_user:%s' % user.username, | |
120 | None, request.ip_addr) |
|
117 | None, request.ip_addr) | |
121 |
|
|
118 | webutils.flash(_('Created user %s') % user.username, | |
122 | category='success') |
|
119 | category='success') | |
123 | Session().commit() |
|
120 | meta.Session().commit() | |
124 | except formencode.Invalid as errors: |
|
121 | except formencode.Invalid as errors: | |
125 | return htmlfill.render( |
|
122 | return htmlfill.render( | |
126 | render('admin/users/user_add.html'), |
|
123 | base.render('admin/users/user_add.html'), | |
127 | defaults=errors.value, |
|
124 | defaults=errors.value, | |
128 | errors=errors.error_dict or {}, |
|
125 | errors=errors.error_dict or {}, | |
129 | prefix_error=False, |
|
126 | prefix_error=False, | |
130 | encoding="UTF-8", |
|
127 | encoding="UTF-8", | |
131 | force_defaults=False) |
|
128 | force_defaults=False) | |
132 | except UserCreationError as e: |
|
129 | except UserCreationError as e: | |
133 |
|
|
130 | webutils.flash(e, 'error') | |
134 | except Exception: |
|
131 | except Exception: | |
135 | log.error(traceback.format_exc()) |
|
132 | log.error(traceback.format_exc()) | |
136 |
|
|
133 | webutils.flash(_('Error occurred during creation of user %s') | |
137 | % request.POST.get('username'), category='error') |
|
134 | % request.POST.get('username'), category='error') | |
138 | raise HTTPFound(location=url('edit_user', id=user.user_id)) |
|
135 | raise HTTPFound(location=url('edit_user', id=user.user_id)) | |
139 |
|
136 | |||
140 | def new(self, format='html'): |
|
137 | def new(self, format='html'): | |
141 | c.default_extern_type = User.DEFAULT_AUTH_TYPE |
|
138 | c.default_extern_type = db.User.DEFAULT_AUTH_TYPE | |
142 | c.default_extern_name = '' |
|
139 | c.default_extern_name = '' | |
143 | return render('admin/users/user_add.html') |
|
140 | return base.render('admin/users/user_add.html') | |
144 |
|
141 | |||
145 | def update(self, id): |
|
142 | def update(self, id): | |
146 | user_model = UserModel() |
|
143 | user_model = UserModel() | |
@@ -155,10 +152,10 b' class UsersController(BaseController):' | |||||
155 |
|
152 | |||
156 | user_model.update(id, form_result, skip_attrs=skip_attrs) |
|
153 | user_model.update(id, form_result, skip_attrs=skip_attrs) | |
157 | usr = form_result['username'] |
|
154 | usr = form_result['username'] | |
158 | action_logger(request.authuser, 'admin_updated_user:%s' % usr, |
|
155 | userlog.action_logger(request.authuser, 'admin_updated_user:%s' % usr, | |
159 | None, request.ip_addr) |
|
156 | None, request.ip_addr) | |
160 |
|
|
157 | webutils.flash(_('User updated successfully'), category='success') | |
161 | Session().commit() |
|
158 | meta.Session().commit() | |
162 | except formencode.Invalid as errors: |
|
159 | except formencode.Invalid as errors: | |
163 | defaults = errors.value |
|
160 | defaults = errors.value | |
164 | e = errors.error_dict or {} |
|
161 | e = errors.error_dict or {} | |
@@ -176,29 +173,33 b' class UsersController(BaseController):' | |||||
176 | force_defaults=False) |
|
173 | force_defaults=False) | |
177 | except Exception: |
|
174 | except Exception: | |
178 | log.error(traceback.format_exc()) |
|
175 | log.error(traceback.format_exc()) | |
179 |
|
|
176 | webutils.flash(_('Error occurred during update of user %s') | |
180 | % form_result.get('username'), category='error') |
|
177 | % form_result.get('username'), category='error') | |
181 | raise HTTPFound(location=url('edit_user', id=id)) |
|
178 | raise HTTPFound(location=url('edit_user', id=id)) | |
182 |
|
179 | |||
183 | def delete(self, id): |
|
180 | def delete(self, id): | |
184 | usr = User.get_or_404(id) |
|
181 | usr = db.User.get_or_404(id) | |
|
182 | has_ssh_keys = bool(usr.ssh_keys) | |||
185 | try: |
|
183 | try: | |
186 | UserModel().delete(usr) |
|
184 | UserModel().delete(usr) | |
187 | Session().commit() |
|
185 | meta.Session().commit() | |
188 |
|
|
186 | webutils.flash(_('Successfully deleted user'), category='success') | |
189 | except (UserOwnsReposException, DefaultUserException) as e: |
|
187 | except (UserOwnsReposException, DefaultUserException) as e: | |
190 |
|
|
188 | webutils.flash(e, category='warning') | |
191 | except Exception: |
|
189 | except Exception: | |
192 | log.error(traceback.format_exc()) |
|
190 | log.error(traceback.format_exc()) | |
193 |
|
|
191 | webutils.flash(_('An error occurred during deletion of user'), | |
194 | category='error') |
|
192 | category='error') | |
|
193 | else: | |||
|
194 | if has_ssh_keys: | |||
|
195 | SshKeyModel().write_authorized_keys() | |||
195 | raise HTTPFound(location=url('users')) |
|
196 | raise HTTPFound(location=url('users')) | |
196 |
|
197 | |||
197 | def _get_user_or_raise_if_default(self, id): |
|
198 | def _get_user_or_raise_if_default(self, id): | |
198 | try: |
|
199 | try: | |
199 | return User.get_or_404(id, allow_default=False) |
|
200 | return db.User.get_or_404(id, allow_default=False) | |
200 | except DefaultUserException: |
|
201 | except DefaultUserException: | |
201 |
|
|
202 | webutils.flash(_("The default user cannot be edited"), category='warning') | |
202 | raise HTTPNotFound |
|
203 | raise HTTPNotFound | |
203 |
|
204 | |||
204 | def _render_edit_profile(self, user): |
|
205 | def _render_edit_profile(self, user): | |
@@ -207,7 +208,7 b' class UsersController(BaseController):' | |||||
207 | c.perm_user = AuthUser(dbuser=user) |
|
208 | c.perm_user = AuthUser(dbuser=user) | |
208 | managed_fields = auth_modules.get_managed_fields(user) |
|
209 | managed_fields = auth_modules.get_managed_fields(user) | |
209 | c.readonly = lambda n: 'readonly' if n in managed_fields else None |
|
210 | c.readonly = lambda n: 'readonly' if n in managed_fields else None | |
210 | return render('admin/users/user_edit.html') |
|
211 | return base.render('admin/users/user_edit.html') | |
211 |
|
212 | |||
212 | def edit(self, id, format='html'): |
|
213 | def edit(self, id, format='html'): | |
213 | user = self._get_user_or_raise_if_default(id) |
|
214 | user = self._get_user_or_raise_if_default(id) | |
@@ -233,7 +234,7 b' class UsersController(BaseController):' | |||||
233 | 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'), |
|
234 | 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'), | |
234 | }) |
|
235 | }) | |
235 | return htmlfill.render( |
|
236 | return htmlfill.render( | |
236 | render('admin/users/user_edit.html'), |
|
237 | base.render('admin/users/user_edit.html'), | |
237 | defaults=defaults, |
|
238 | defaults=defaults, | |
238 | encoding="UTF-8", |
|
239 | encoding="UTF-8", | |
239 | force_defaults=False) |
|
240 | force_defaults=False) | |
@@ -254,7 +255,7 b' class UsersController(BaseController):' | |||||
254 | show_expired=show_expired) |
|
255 | show_expired=show_expired) | |
255 | defaults = c.user.get_dict() |
|
256 | defaults = c.user.get_dict() | |
256 | return htmlfill.render( |
|
257 | return htmlfill.render( | |
257 | render('admin/users/user_edit.html'), |
|
258 | base.render('admin/users/user_edit.html'), | |
258 | defaults=defaults, |
|
259 | defaults=defaults, | |
259 | encoding="UTF-8", |
|
260 | encoding="UTF-8", | |
260 | force_defaults=False) |
|
261 | force_defaults=False) | |
@@ -265,8 +266,8 b' class UsersController(BaseController):' | |||||
265 | lifetime = safe_int(request.POST.get('lifetime'), -1) |
|
266 | lifetime = safe_int(request.POST.get('lifetime'), -1) | |
266 | description = request.POST.get('description') |
|
267 | description = request.POST.get('description') | |
267 | ApiKeyModel().create(c.user.user_id, description, lifetime) |
|
268 | ApiKeyModel().create(c.user.user_id, description, lifetime) | |
268 | Session().commit() |
|
269 | meta.Session().commit() | |
269 |
|
|
270 | webutils.flash(_("API key successfully created"), category='success') | |
270 | raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id)) |
|
271 | raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id)) | |
271 |
|
272 | |||
272 | def delete_api_key(self, id): |
|
273 | def delete_api_key(self, id): | |
@@ -275,12 +276,12 b' class UsersController(BaseController):' | |||||
275 | api_key = request.POST.get('del_api_key') |
|
276 | api_key = request.POST.get('del_api_key') | |
276 | if request.POST.get('del_api_key_builtin'): |
|
277 | if request.POST.get('del_api_key_builtin'): | |
277 | c.user.api_key = generate_api_key() |
|
278 | c.user.api_key = generate_api_key() | |
278 | Session().commit() |
|
279 | meta.Session().commit() | |
279 |
|
|
280 | webutils.flash(_("API key successfully reset"), category='success') | |
280 | elif api_key: |
|
281 | elif api_key: | |
281 | ApiKeyModel().delete(api_key, c.user.user_id) |
|
282 | ApiKeyModel().delete(api_key, c.user.user_id) | |
282 | Session().commit() |
|
283 | meta.Session().commit() | |
283 |
|
|
284 | webutils.flash(_("API key successfully deleted"), category='success') | |
284 |
|
285 | |||
285 | raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id)) |
|
286 | raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id)) | |
286 |
|
287 | |||
@@ -301,7 +302,7 b' class UsersController(BaseController):' | |||||
301 | 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'), |
|
302 | 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'), | |
302 | }) |
|
303 | }) | |
303 | return htmlfill.render( |
|
304 | return htmlfill.render( | |
304 | render('admin/users/user_edit.html'), |
|
305 | base.render('admin/users/user_edit.html'), | |
305 | defaults=defaults, |
|
306 | defaults=defaults, | |
306 | encoding="UTF-8", |
|
307 | encoding="UTF-8", | |
307 | force_defaults=False) |
|
308 | force_defaults=False) | |
@@ -315,11 +316,11 b' class UsersController(BaseController):' | |||||
315 |
|
316 | |||
316 | user_model = UserModel() |
|
317 | user_model = UserModel() | |
317 |
|
318 | |||
318 | defs = UserToPerm.query() \ |
|
319 | defs = db.UserToPerm.query() \ | |
319 | .filter(UserToPerm.user == user) \ |
|
320 | .filter(db.UserToPerm.user == user) \ | |
320 | .all() |
|
321 | .all() | |
321 | for ug in defs: |
|
322 | for ug in defs: | |
322 | Session().delete(ug) |
|
323 | meta.Session().delete(ug) | |
323 |
|
324 | |||
324 | if form_result['create_repo_perm']: |
|
325 | if form_result['create_repo_perm']: | |
325 | user_model.grant_perm(id, 'hg.create.repository') |
|
326 | user_model.grant_perm(id, 'hg.create.repository') | |
@@ -333,23 +334,23 b' class UsersController(BaseController):' | |||||
333 | user_model.grant_perm(id, 'hg.fork.repository') |
|
334 | user_model.grant_perm(id, 'hg.fork.repository') | |
334 | else: |
|
335 | else: | |
335 | user_model.grant_perm(id, 'hg.fork.none') |
|
336 | user_model.grant_perm(id, 'hg.fork.none') | |
336 |
|
|
337 | webutils.flash(_("Updated permissions"), category='success') | |
337 | Session().commit() |
|
338 | meta.Session().commit() | |
338 | except Exception: |
|
339 | except Exception: | |
339 | log.error(traceback.format_exc()) |
|
340 | log.error(traceback.format_exc()) | |
340 |
|
|
341 | webutils.flash(_('An error occurred during permissions saving'), | |
341 | category='error') |
|
342 | category='error') | |
342 | raise HTTPFound(location=url('edit_user_perms', id=id)) |
|
343 | raise HTTPFound(location=url('edit_user_perms', id=id)) | |
343 |
|
344 | |||
344 | def edit_emails(self, id): |
|
345 | def edit_emails(self, id): | |
345 | c.user = self._get_user_or_raise_if_default(id) |
|
346 | c.user = self._get_user_or_raise_if_default(id) | |
346 | c.active = 'emails' |
|
347 | c.active = 'emails' | |
347 | c.user_email_map = UserEmailMap.query() \ |
|
348 | c.user_email_map = db.UserEmailMap.query() \ | |
348 | .filter(UserEmailMap.user == c.user).all() |
|
349 | .filter(db.UserEmailMap.user == c.user).all() | |
349 |
|
350 | |||
350 | defaults = c.user.get_dict() |
|
351 | defaults = c.user.get_dict() | |
351 | return htmlfill.render( |
|
352 | return htmlfill.render( | |
352 | render('admin/users/user_edit.html'), |
|
353 | base.render('admin/users/user_edit.html'), | |
353 | defaults=defaults, |
|
354 | defaults=defaults, | |
354 | encoding="UTF-8", |
|
355 | encoding="UTF-8", | |
355 | force_defaults=False) |
|
356 | force_defaults=False) | |
@@ -361,14 +362,14 b' class UsersController(BaseController):' | |||||
361 |
|
362 | |||
362 | try: |
|
363 | try: | |
363 | user_model.add_extra_email(id, email) |
|
364 | user_model.add_extra_email(id, email) | |
364 | Session().commit() |
|
365 | meta.Session().commit() | |
365 |
|
|
366 | webutils.flash(_("Added email %s to user") % email, category='success') | |
366 | except formencode.Invalid as error: |
|
367 | except formencode.Invalid as error: | |
367 | msg = error.error_dict['email'] |
|
368 | msg = error.error_dict['email'] | |
368 |
|
|
369 | webutils.flash(msg, category='error') | |
369 | except Exception: |
|
370 | except Exception: | |
370 | log.error(traceback.format_exc()) |
|
371 | log.error(traceback.format_exc()) | |
371 |
|
|
372 | webutils.flash(_('An error occurred during email saving'), | |
372 | category='error') |
|
373 | category='error') | |
373 | raise HTTPFound(location=url('edit_user_emails', id=id)) |
|
374 | raise HTTPFound(location=url('edit_user_emails', id=id)) | |
374 |
|
375 | |||
@@ -377,22 +378,22 b' class UsersController(BaseController):' | |||||
377 | email_id = request.POST.get('del_email_id') |
|
378 | email_id = request.POST.get('del_email_id') | |
378 | user_model = UserModel() |
|
379 | user_model = UserModel() | |
379 | user_model.delete_extra_email(id, email_id) |
|
380 | user_model.delete_extra_email(id, email_id) | |
380 | Session().commit() |
|
381 | meta.Session().commit() | |
381 |
|
|
382 | webutils.flash(_("Removed email from user"), category='success') | |
382 | raise HTTPFound(location=url('edit_user_emails', id=id)) |
|
383 | raise HTTPFound(location=url('edit_user_emails', id=id)) | |
383 |
|
384 | |||
384 | def edit_ips(self, id): |
|
385 | def edit_ips(self, id): | |
385 | c.user = self._get_user_or_raise_if_default(id) |
|
386 | c.user = self._get_user_or_raise_if_default(id) | |
386 | c.active = 'ips' |
|
387 | c.active = 'ips' | |
387 | c.user_ip_map = UserIpMap.query() \ |
|
388 | c.user_ip_map = db.UserIpMap.query() \ | |
388 | .filter(UserIpMap.user == c.user).all() |
|
389 | .filter(db.UserIpMap.user == c.user).all() | |
389 |
|
390 | |||
390 | c.default_user_ip_map = UserIpMap.query() \ |
|
391 | c.default_user_ip_map = db.UserIpMap.query() \ | |
391 | .filter(UserIpMap.user_id == kallithea.DEFAULT_USER_ID).all() |
|
392 | .filter(db.UserIpMap.user_id == kallithea.DEFAULT_USER_ID).all() | |
392 |
|
393 | |||
393 | defaults = c.user.get_dict() |
|
394 | defaults = c.user.get_dict() | |
394 | return htmlfill.render( |
|
395 | return htmlfill.render( | |
395 | render('admin/users/user_edit.html'), |
|
396 | base.render('admin/users/user_edit.html'), | |
396 | defaults=defaults, |
|
397 | defaults=defaults, | |
397 | encoding="UTF-8", |
|
398 | encoding="UTF-8", | |
398 | force_defaults=False) |
|
399 | force_defaults=False) | |
@@ -403,14 +404,14 b' class UsersController(BaseController):' | |||||
403 |
|
404 | |||
404 | try: |
|
405 | try: | |
405 | user_model.add_extra_ip(id, ip) |
|
406 | user_model.add_extra_ip(id, ip) | |
406 | Session().commit() |
|
407 | meta.Session().commit() | |
407 |
|
|
408 | webutils.flash(_("Added IP address %s to user whitelist") % ip, category='success') | |
408 | except formencode.Invalid as error: |
|
409 | except formencode.Invalid as error: | |
409 | msg = error.error_dict['ip'] |
|
410 | msg = error.error_dict['ip'] | |
410 |
|
|
411 | webutils.flash(msg, category='error') | |
411 | except Exception: |
|
412 | except Exception: | |
412 | log.error(traceback.format_exc()) |
|
413 | log.error(traceback.format_exc()) | |
413 |
|
|
414 | webutils.flash(_('An error occurred while adding IP address'), | |
414 | category='error') |
|
415 | category='error') | |
415 |
|
416 | |||
416 | if 'default_user' in request.POST: |
|
417 | if 'default_user' in request.POST: | |
@@ -421,26 +422,26 b' class UsersController(BaseController):' | |||||
421 | ip_id = request.POST.get('del_ip_id') |
|
422 | ip_id = request.POST.get('del_ip_id') | |
422 | user_model = UserModel() |
|
423 | user_model = UserModel() | |
423 | user_model.delete_extra_ip(id, ip_id) |
|
424 | user_model.delete_extra_ip(id, ip_id) | |
424 | Session().commit() |
|
425 | meta.Session().commit() | |
425 |
|
|
426 | webutils.flash(_("Removed IP address from user whitelist"), category='success') | |
426 |
|
427 | |||
427 | if 'default_user' in request.POST: |
|
428 | if 'default_user' in request.POST: | |
428 | raise HTTPFound(location=url('admin_permissions_ips')) |
|
429 | raise HTTPFound(location=url('admin_permissions_ips')) | |
429 | raise HTTPFound(location=url('edit_user_ips', id=id)) |
|
430 | raise HTTPFound(location=url('edit_user_ips', id=id)) | |
430 |
|
431 | |||
431 | @IfSshEnabled |
|
432 | @base.IfSshEnabled | |
432 | def edit_ssh_keys(self, id): |
|
433 | def edit_ssh_keys(self, id): | |
433 | c.user = self._get_user_or_raise_if_default(id) |
|
434 | c.user = self._get_user_or_raise_if_default(id) | |
434 | c.active = 'ssh_keys' |
|
435 | c.active = 'ssh_keys' | |
435 | c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id) |
|
436 | c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id) | |
436 | defaults = c.user.get_dict() |
|
437 | defaults = c.user.get_dict() | |
437 | return htmlfill.render( |
|
438 | return htmlfill.render( | |
438 | render('admin/users/user_edit.html'), |
|
439 | base.render('admin/users/user_edit.html'), | |
439 | defaults=defaults, |
|
440 | defaults=defaults, | |
440 | encoding="UTF-8", |
|
441 | encoding="UTF-8", | |
441 | force_defaults=False) |
|
442 | force_defaults=False) | |
442 |
|
443 | |||
443 | @IfSshEnabled |
|
444 | @base.IfSshEnabled | |
444 | def ssh_keys_add(self, id): |
|
445 | def ssh_keys_add(self, id): | |
445 | c.user = self._get_user_or_raise_if_default(id) |
|
446 | c.user = self._get_user_or_raise_if_default(id) | |
446 |
|
447 | |||
@@ -449,23 +450,23 b' class UsersController(BaseController):' | |||||
449 | try: |
|
450 | try: | |
450 | new_ssh_key = SshKeyModel().create(c.user.user_id, |
|
451 | new_ssh_key = SshKeyModel().create(c.user.user_id, | |
451 | description, public_key) |
|
452 | description, public_key) | |
452 | Session().commit() |
|
453 | meta.Session().commit() | |
453 | SshKeyModel().write_authorized_keys() |
|
454 | SshKeyModel().write_authorized_keys() | |
454 |
|
|
455 | webutils.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success') | |
455 | except SshKeyModelException as e: |
|
456 | except SshKeyModelException as e: | |
456 |
|
|
457 | webutils.flash(e.args[0], category='error') | |
457 | raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id)) |
|
458 | raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id)) | |
458 |
|
459 | |||
459 | @IfSshEnabled |
|
460 | @base.IfSshEnabled | |
460 | def ssh_keys_delete(self, id): |
|
461 | def ssh_keys_delete(self, id): | |
461 | c.user = self._get_user_or_raise_if_default(id) |
|
462 | c.user = self._get_user_or_raise_if_default(id) | |
462 |
|
463 | |||
463 | fingerprint = request.POST.get('del_public_key_fingerprint') |
|
464 | fingerprint = request.POST.get('del_public_key_fingerprint') | |
464 | try: |
|
465 | try: | |
465 | SshKeyModel().delete(fingerprint, c.user.user_id) |
|
466 | SshKeyModel().delete(fingerprint, c.user.user_id) | |
466 | Session().commit() |
|
467 | meta.Session().commit() | |
467 | SshKeyModel().write_authorized_keys() |
|
468 | SshKeyModel().write_authorized_keys() | |
468 |
|
|
469 | webutils.flash(_("SSH key successfully deleted"), category='success') | |
469 | except SshKeyModelException as e: |
|
470 | except SshKeyModelException as e: | |
470 |
|
|
471 | webutils.flash(e.args[0], category='error') | |
471 | raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id)) |
|
472 | raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id)) |
@@ -35,12 +35,11 b' import types' | |||||
35 | from tg import Response, TGController, request, response |
|
35 | from tg import Response, TGController, request, response | |
36 | from webob.exc import HTTPError, HTTPException |
|
36 | from webob.exc import HTTPError, HTTPException | |
37 |
|
37 | |||
|
38 | from kallithea.controllers import base | |||
38 | from kallithea.lib import ext_json |
|
39 | from kallithea.lib import ext_json | |
39 | from kallithea.lib.auth import AuthUser |
|
40 | from kallithea.lib.auth import AuthUser | |
40 | from kallithea.lib.base import _get_ip_addr as _get_ip |
|
|||
41 | from kallithea.lib.base import get_path_info |
|
|||
42 | from kallithea.lib.utils2 import ascii_bytes |
|
41 | from kallithea.lib.utils2 import ascii_bytes | |
43 |
from kallithea.model |
|
42 | from kallithea.model import db | |
44 |
|
43 | |||
45 |
|
44 | |||
46 | log = logging.getLogger('JSONRPC') |
|
45 | log = logging.getLogger('JSONRPC') | |
@@ -83,9 +82,6 b' class JSONRPCController(TGController):' | |||||
83 |
|
82 | |||
84 | """ |
|
83 | """ | |
85 |
|
84 | |||
86 | def _get_ip_addr(self, environ): |
|
|||
87 | return _get_ip(environ) |
|
|||
88 |
|
||||
89 | def _get_method_args(self): |
|
85 | def _get_method_args(self): | |
90 | """ |
|
86 | """ | |
91 | Return `self._rpc_args` to dispatched controller method |
|
87 | Return `self._rpc_args` to dispatched controller method | |
@@ -103,7 +99,7 b' class JSONRPCController(TGController):' | |||||
103 |
|
99 | |||
104 | environ = state.request.environ |
|
100 | environ = state.request.environ | |
105 | start = time.time() |
|
101 | start = time.time() | |
106 |
ip_addr = se |
|
102 | ip_addr = base.get_ip_addr(environ) | |
107 | self._req_id = None |
|
103 | self._req_id = None | |
108 | if 'CONTENT_LENGTH' not in environ: |
|
104 | if 'CONTENT_LENGTH' not in environ: | |
109 | log.debug("No Content-Length") |
|
105 | log.debug("No Content-Length") | |
@@ -145,7 +141,7 b' class JSONRPCController(TGController):' | |||||
145 |
|
141 | |||
146 | # check if we can find this session using api_key |
|
142 | # check if we can find this session using api_key | |
147 | try: |
|
143 | try: | |
148 | u = User.get_by_api_key(self._req_api_key) |
|
144 | u = db.User.get_by_api_key(self._req_api_key) | |
149 | auth_user = AuthUser.make(dbuser=u, ip_addr=ip_addr) |
|
145 | auth_user = AuthUser.make(dbuser=u, ip_addr=ip_addr) | |
150 | if auth_user is None: |
|
146 | if auth_user is None: | |
151 | raise JSONRPCErrorResponse(retid=self._req_id, |
|
147 | raise JSONRPCErrorResponse(retid=self._req_id, | |
@@ -208,8 +204,8 b' class JSONRPCController(TGController):' | |||||
208 | self._rpc_args['environ'] = environ |
|
204 | self._rpc_args['environ'] = environ | |
209 |
|
205 | |||
210 | log.info('IP: %s Request to %s time: %.3fs' % ( |
|
206 | log.info('IP: %s Request to %s time: %.3fs' % ( | |
211 |
se |
|
207 | base.get_ip_addr(environ), | |
212 | get_path_info(environ), time.time() - start) |
|
208 | base.get_path_info(environ), time.time() - start) | |
213 | ) |
|
209 | ) | |
214 |
|
210 | |||
215 | state.set_action(self._rpc_call, []) |
|
211 | state.set_action(self._rpc_call, []) |
@@ -35,15 +35,13 b' from kallithea.controllers.api import JS' | |||||
35 | from kallithea.lib.auth import (AuthUser, HasPermissionAny, HasPermissionAnyDecorator, HasRepoGroupPermissionLevel, HasRepoPermissionLevel, |
|
35 | from kallithea.lib.auth import (AuthUser, HasPermissionAny, HasPermissionAnyDecorator, HasRepoGroupPermissionLevel, HasRepoPermissionLevel, | |
36 | HasUserGroupPermissionLevel) |
|
36 | HasUserGroupPermissionLevel) | |
37 | from kallithea.lib.exceptions import DefaultUserException, UserGroupsAssignedException |
|
37 | from kallithea.lib.exceptions import DefaultUserException, UserGroupsAssignedException | |
38 |
from kallithea.lib.utils import |
|
38 | from kallithea.lib.utils import repo2db_mapper | |
39 | from kallithea.lib.utils2 import OAttr, Optional |
|
|||
40 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
39 | from kallithea.lib.vcs.backends.base import EmptyChangeset | |
41 | from kallithea.lib.vcs.exceptions import EmptyRepositoryError |
|
40 | from kallithea.lib.vcs.exceptions import EmptyRepositoryError | |
|
41 | from kallithea.model import db, meta, userlog | |||
42 | from kallithea.model.changeset_status import ChangesetStatusModel |
|
42 | from kallithea.model.changeset_status import ChangesetStatusModel | |
43 | from kallithea.model.comment import ChangesetCommentsModel |
|
43 | from kallithea.model.comment import ChangesetCommentsModel | |
44 | from kallithea.model.db import ChangesetStatus, Gist, Permission, PullRequest, RepoGroup, Repository, Setting, User, UserGroup, UserIpMap |
|
|||
45 | from kallithea.model.gist import GistModel |
|
44 | from kallithea.model.gist import GistModel | |
46 | from kallithea.model.meta import Session |
|
|||
47 | from kallithea.model.pull_request import PullRequestModel |
|
45 | from kallithea.model.pull_request import PullRequestModel | |
48 | from kallithea.model.repo import RepoModel |
|
46 | from kallithea.model.repo import RepoModel | |
49 | from kallithea.model.repo_group import RepoGroupModel |
|
47 | from kallithea.model.repo_group import RepoGroupModel | |
@@ -57,10 +55,10 b' log = logging.getLogger(__name__)' | |||||
57 |
|
55 | |||
58 | def store_update(updates, attr, name): |
|
56 | def store_update(updates, attr, name): | |
59 | """ |
|
57 | """ | |
60 |
Stores param in updates dict if it's not |
|
58 | Stores param in updates dict if it's not None (i.e. if user explicitly set | |
61 | allows easy updates of passed in params |
|
59 | a parameter). This allows easy updates of passed in params. | |
62 | """ |
|
60 | """ | |
63 | if not isinstance(attr, Optional): |
|
61 | if attr is not None: | |
64 | updates[name] = attr |
|
62 | updates[name] = attr | |
65 |
|
63 | |||
66 |
|
64 | |||
@@ -94,7 +92,7 b' def get_repo_group_or_error(repogroupid)' | |||||
94 |
|
92 | |||
95 | :param repogroupid: |
|
93 | :param repogroupid: | |
96 | """ |
|
94 | """ | |
97 | repo_group = RepoGroup.guess_instance(repogroupid) |
|
95 | repo_group = db.RepoGroup.guess_instance(repogroupid) | |
98 | if repo_group is None: |
|
96 | if repo_group is None: | |
99 | raise JSONRPCError( |
|
97 | raise JSONRPCError( | |
100 | 'repository group `%s` does not exist' % (repogroupid,)) |
|
98 | 'repository group `%s` does not exist' % (repogroupid,)) | |
@@ -119,7 +117,7 b' def get_perm_or_error(permid, prefix=Non' | |||||
119 |
|
117 | |||
120 | :param permid: |
|
118 | :param permid: | |
121 | """ |
|
119 | """ | |
122 | perm = Permission.get_by_key(permid) |
|
120 | perm = db.Permission.get_by_key(permid) | |
123 | if perm is None: |
|
121 | if perm is None: | |
124 | raise JSONRPCError('permission `%s` does not exist' % (permid,)) |
|
122 | raise JSONRPCError('permission `%s` does not exist' % (permid,)) | |
125 | if prefix: |
|
123 | if prefix: | |
@@ -161,7 +159,7 b' class ApiController(JSONRPCController):' | |||||
161 | return args |
|
159 | return args | |
162 |
|
160 | |||
163 | @HasPermissionAnyDecorator('hg.admin') |
|
161 | @HasPermissionAnyDecorator('hg.admin') | |
164 |
def pull(self, repoid, clone_uri= |
|
162 | def pull(self, repoid, clone_uri=None): | |
165 | """ |
|
163 | """ | |
166 | Triggers a pull from remote location on given repo. Can be used to |
|
164 | Triggers a pull from remote location on given repo. Can be used to | |
167 | automatically keep remote repos up to date. This command can be executed |
|
165 | automatically keep remote repos up to date. This command can be executed | |
@@ -197,7 +195,7 b' class ApiController(JSONRPCController):' | |||||
197 | ScmModel().pull_changes(repo.repo_name, |
|
195 | ScmModel().pull_changes(repo.repo_name, | |
198 | request.authuser.username, |
|
196 | request.authuser.username, | |
199 | request.ip_addr, |
|
197 | request.ip_addr, | |
200 |
clone_uri= |
|
198 | clone_uri=clone_uri) | |
201 | return dict( |
|
199 | return dict( | |
202 | msg='Pulled from `%s`' % repo.repo_name, |
|
200 | msg='Pulled from `%s`' % repo.repo_name, | |
203 | repository=repo.repo_name |
|
201 | repository=repo.repo_name | |
@@ -209,7 +207,7 b' class ApiController(JSONRPCController):' | |||||
209 | ) |
|
207 | ) | |
210 |
|
208 | |||
211 | @HasPermissionAnyDecorator('hg.admin') |
|
209 | @HasPermissionAnyDecorator('hg.admin') | |
212 |
def rescan_repos(self, remove_obsolete= |
|
210 | def rescan_repos(self, remove_obsolete=False): | |
213 | """ |
|
211 | """ | |
214 | Triggers rescan repositories action. If remove_obsolete is set |
|
212 | Triggers rescan repositories action. If remove_obsolete is set | |
215 | than also delete repos that are in database but not in the filesystem. |
|
213 | than also delete repos that are in database but not in the filesystem. | |
@@ -240,7 +238,7 b' class ApiController(JSONRPCController):' | |||||
240 | """ |
|
238 | """ | |
241 |
|
239 | |||
242 | try: |
|
240 | try: | |
243 |
rm_obsolete = |
|
241 | rm_obsolete = remove_obsolete | |
244 | added, removed = repo2db_mapper(ScmModel().repo_scan(), |
|
242 | added, removed = repo2db_mapper(ScmModel().repo_scan(), | |
245 | remove_obsolete=rm_obsolete) |
|
243 | remove_obsolete=rm_obsolete) | |
246 | return {'added': added, 'removed': removed} |
|
244 | return {'added': added, 'removed': removed} | |
@@ -295,7 +293,7 b' class ApiController(JSONRPCController):' | |||||
295 | ) |
|
293 | ) | |
296 |
|
294 | |||
297 | @HasPermissionAnyDecorator('hg.admin') |
|
295 | @HasPermissionAnyDecorator('hg.admin') | |
298 |
def get_ip(self, userid= |
|
296 | def get_ip(self, userid=None): | |
299 | """ |
|
297 | """ | |
300 | Shows IP address as seen from Kallithea server, together with all |
|
298 | Shows IP address as seen from Kallithea server, together with all | |
301 | defined IP addresses for given user. If userid is not passed data is |
|
299 | defined IP addresses for given user. If userid is not passed data is | |
@@ -321,10 +319,10 b' class ApiController(JSONRPCController):' | |||||
321 | } |
|
319 | } | |
322 |
|
320 | |||
323 | """ |
|
321 | """ | |
324 |
if |
|
322 | if userid is None: | |
325 | userid = request.authuser.user_id |
|
323 | userid = request.authuser.user_id | |
326 | user = get_user_or_error(userid) |
|
324 | user = get_user_or_error(userid) | |
327 | ips = UserIpMap.query().filter(UserIpMap.user == user).all() |
|
325 | ips = db.UserIpMap.query().filter(db.UserIpMap.user == user).all() | |
328 | return dict( |
|
326 | return dict( | |
329 | server_ip_addr=request.ip_addr, |
|
327 | server_ip_addr=request.ip_addr, | |
330 | user_ips=ips |
|
328 | user_ips=ips | |
@@ -350,9 +348,9 b' class ApiController(JSONRPCController):' | |||||
350 | } |
|
348 | } | |
351 | error : null |
|
349 | error : null | |
352 | """ |
|
350 | """ | |
353 | return Setting.get_server_info() |
|
351 | return db.Setting.get_server_info() | |
354 |
|
352 | |||
355 |
def get_user(self, userid= |
|
353 | def get_user(self, userid=None): | |
356 | """ |
|
354 | """ | |
357 | Gets a user by username or user_id, Returns empty result if user is |
|
355 | Gets a user by username or user_id, Returns empty result if user is | |
358 | not found. If userid param is skipped it is set to id of user who is |
|
356 | not found. If userid param is skipped it is set to id of user who is | |
@@ -397,12 +395,12 b' class ApiController(JSONRPCController):' | |||||
397 | if not HasPermissionAny('hg.admin')(): |
|
395 | if not HasPermissionAny('hg.admin')(): | |
398 | # make sure normal user does not pass someone else userid, |
|
396 | # make sure normal user does not pass someone else userid, | |
399 | # he is not allowed to do that |
|
397 | # he is not allowed to do that | |
400 |
if not |
|
398 | if userid is not None and userid != request.authuser.user_id: | |
401 | raise JSONRPCError( |
|
399 | raise JSONRPCError( | |
402 | 'userid is not the same as your user' |
|
400 | 'userid is not the same as your user' | |
403 | ) |
|
401 | ) | |
404 |
|
402 | |||
405 |
if |
|
403 | if userid is None: | |
406 | userid = request.authuser.user_id |
|
404 | userid = request.authuser.user_id | |
407 |
|
405 | |||
408 | user = get_user_or_error(userid) |
|
406 | user = get_user_or_error(userid) | |
@@ -426,17 +424,17 b' class ApiController(JSONRPCController):' | |||||
426 |
|
424 | |||
427 | return [ |
|
425 | return [ | |
428 | user.get_api_data() |
|
426 | user.get_api_data() | |
429 | for user in User.query() |
|
427 | for user in db.User.query() | |
430 | .order_by(User.username) |
|
428 | .order_by(db.User.username) | |
431 | .filter_by(is_default_user=False) |
|
429 | .filter_by(is_default_user=False) | |
432 | ] |
|
430 | ] | |
433 |
|
431 | |||
434 | @HasPermissionAnyDecorator('hg.admin') |
|
432 | @HasPermissionAnyDecorator('hg.admin') | |
435 |
def create_user(self, username, email, password= |
|
433 | def create_user(self, username, email, password='', | |
436 |
firstname= |
|
434 | firstname='', lastname='', | |
437 |
active= |
|
435 | active=True, admin=False, | |
438 |
extern_type= |
|
436 | extern_type=db.User.DEFAULT_AUTH_TYPE, | |
439 |
extern_name= |
|
437 | extern_name=''): | |
440 | """ |
|
438 | """ | |
441 | Creates new user. Returns new user object. This command can |
|
439 | Creates new user. Returns new user object. This command can | |
442 | be executed only using api_key belonging to user with admin rights. |
|
440 | be executed only using api_key belonging to user with admin rights. | |
@@ -484,25 +482,25 b' class ApiController(JSONRPCController):' | |||||
484 |
|
482 | |||
485 | """ |
|
483 | """ | |
486 |
|
484 | |||
487 | if User.get_by_username(username): |
|
485 | if db.User.get_by_username(username): | |
488 | raise JSONRPCError("user `%s` already exist" % (username,)) |
|
486 | raise JSONRPCError("user `%s` already exist" % (username,)) | |
489 |
|
487 | |||
490 | if User.get_by_email(email): |
|
488 | if db.User.get_by_email(email): | |
491 | raise JSONRPCError("email `%s` already exist" % (email,)) |
|
489 | raise JSONRPCError("email `%s` already exist" % (email,)) | |
492 |
|
490 | |||
493 | try: |
|
491 | try: | |
494 | user = UserModel().create_or_update( |
|
492 | user = UserModel().create_or_update( | |
495 |
username= |
|
493 | username=username, | |
496 |
password= |
|
494 | password=password, | |
497 |
email= |
|
495 | email=email, | |
498 |
firstname= |
|
496 | firstname=firstname, | |
499 |
lastname= |
|
497 | lastname=lastname, | |
500 |
active= |
|
498 | active=active, | |
501 |
admin= |
|
499 | admin=admin, | |
502 |
extern_type= |
|
500 | extern_type=extern_type, | |
503 |
extern_name= |
|
501 | extern_name=extern_name | |
504 | ) |
|
502 | ) | |
505 | Session().commit() |
|
503 | meta.Session().commit() | |
506 | return dict( |
|
504 | return dict( | |
507 | msg='created new user `%s`' % username, |
|
505 | msg='created new user `%s`' % username, | |
508 | user=user.get_api_data() |
|
506 | user=user.get_api_data() | |
@@ -512,11 +510,11 b' class ApiController(JSONRPCController):' | |||||
512 | raise JSONRPCError('failed to create user `%s`' % (username,)) |
|
510 | raise JSONRPCError('failed to create user `%s`' % (username,)) | |
513 |
|
511 | |||
514 | @HasPermissionAnyDecorator('hg.admin') |
|
512 | @HasPermissionAnyDecorator('hg.admin') | |
515 |
def update_user(self, userid, username= |
|
513 | def update_user(self, userid, username=None, | |
516 |
email= |
|
514 | email=None, password=None, | |
517 |
firstname= |
|
515 | firstname=None, lastname=None, | |
518 |
active= |
|
516 | active=None, admin=None, | |
519 |
extern_type= |
|
517 | extern_type=None, extern_name=None): | |
520 | """ |
|
518 | """ | |
521 | updates given user if such user exists. This command can |
|
519 | updates given user if such user exists. This command can | |
522 | be executed only using api_key belonging to user with admin rights. |
|
520 | be executed only using api_key belonging to user with admin rights. | |
@@ -580,7 +578,7 b' class ApiController(JSONRPCController):' | |||||
580 | store_update(updates, extern_type, 'extern_type') |
|
578 | store_update(updates, extern_type, 'extern_type') | |
581 |
|
579 | |||
582 | user = UserModel().update_user(user, **updates) |
|
580 | user = UserModel().update_user(user, **updates) | |
583 | Session().commit() |
|
581 | meta.Session().commit() | |
584 | return dict( |
|
582 | return dict( | |
585 | msg='updated user ID:%s %s' % (user.user_id, user.username), |
|
583 | msg='updated user ID:%s %s' % (user.user_id, user.username), | |
586 | user=user.get_api_data() |
|
584 | user=user.get_api_data() | |
@@ -623,7 +621,7 b' class ApiController(JSONRPCController):' | |||||
623 |
|
621 | |||
624 | try: |
|
622 | try: | |
625 | UserModel().delete(userid) |
|
623 | UserModel().delete(userid) | |
626 | Session().commit() |
|
624 | meta.Session().commit() | |
627 | return dict( |
|
625 | return dict( | |
628 | msg='deleted user ID:%s %s' % (user.user_id, user.username), |
|
626 | msg='deleted user ID:%s %s' % (user.user_id, user.username), | |
629 | user=None |
|
627 | user=None | |
@@ -682,12 +680,12 b' class ApiController(JSONRPCController):' | |||||
682 |
|
680 | |||
683 | return [ |
|
681 | return [ | |
684 | user_group.get_api_data() |
|
682 | user_group.get_api_data() | |
685 | for user_group in UserGroupList(UserGroup.query().all(), perm_level='read') |
|
683 | for user_group in UserGroupList(db.UserGroup.query().all(), perm_level='read') | |
686 | ] |
|
684 | ] | |
687 |
|
685 | |||
688 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') |
|
686 | @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') | |
689 |
def create_user_group(self, group_name, description= |
|
687 | def create_user_group(self, group_name, description='', | |
690 |
owner= |
|
688 | owner=None, active=True): | |
691 | """ |
|
689 | """ | |
692 | Creates new user group. This command can be executed only using api_key |
|
690 | Creates new user group. This command can be executed only using api_key | |
693 | belonging to user with admin rights or an user who has create user group |
|
691 | belonging to user with admin rights or an user who has create user group | |
@@ -727,15 +725,13 b' class ApiController(JSONRPCController):' | |||||
727 | raise JSONRPCError("user group `%s` already exist" % (group_name,)) |
|
725 | raise JSONRPCError("user group `%s` already exist" % (group_name,)) | |
728 |
|
726 | |||
729 | try: |
|
727 | try: | |
730 |
if |
|
728 | if owner is None: | |
731 | owner = request.authuser.user_id |
|
729 | owner = request.authuser.user_id | |
732 |
|
730 | |||
733 | owner = get_user_or_error(owner) |
|
731 | owner = get_user_or_error(owner) | |
734 | active = Optional.extract(active) |
|
|||
735 | description = Optional.extract(description) |
|
|||
736 | ug = UserGroupModel().create(name=group_name, description=description, |
|
732 | ug = UserGroupModel().create(name=group_name, description=description, | |
737 | owner=owner, active=active) |
|
733 | owner=owner, active=active) | |
738 | Session().commit() |
|
734 | meta.Session().commit() | |
739 | return dict( |
|
735 | return dict( | |
740 | msg='created new user group `%s`' % group_name, |
|
736 | msg='created new user group `%s`' % group_name, | |
741 | user_group=ug.get_api_data() |
|
737 | user_group=ug.get_api_data() | |
@@ -745,9 +741,9 b' class ApiController(JSONRPCController):' | |||||
745 | raise JSONRPCError('failed to create group `%s`' % (group_name,)) |
|
741 | raise JSONRPCError('failed to create group `%s`' % (group_name,)) | |
746 |
|
742 | |||
747 | # permission check inside |
|
743 | # permission check inside | |
748 |
def update_user_group(self, usergroupid, group_name= |
|
744 | def update_user_group(self, usergroupid, group_name=None, | |
749 |
description= |
|
745 | description=None, owner=None, | |
750 |
active= |
|
746 | active=None): | |
751 | """ |
|
747 | """ | |
752 | Updates given usergroup. This command can be executed only using api_key |
|
748 | Updates given usergroup. This command can be executed only using api_key | |
753 | belonging to user with admin rights or an admin of given user group |
|
749 | belonging to user with admin rights or an admin of given user group | |
@@ -786,7 +782,7 b' class ApiController(JSONRPCController):' | |||||
786 | if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name): |
|
782 | if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name): | |
787 | raise JSONRPCError('user group `%s` does not exist' % (usergroupid,)) |
|
783 | raise JSONRPCError('user group `%s` does not exist' % (usergroupid,)) | |
788 |
|
784 | |||
789 | if not isinstance(owner, Optional): |
|
785 | if owner is not None: | |
790 | owner = get_user_or_error(owner) |
|
786 | owner = get_user_or_error(owner) | |
791 |
|
787 | |||
792 | updates = {} |
|
788 | updates = {} | |
@@ -796,7 +792,7 b' class ApiController(JSONRPCController):' | |||||
796 | store_update(updates, active, 'users_group_active') |
|
792 | store_update(updates, active, 'users_group_active') | |
797 | try: |
|
793 | try: | |
798 | UserGroupModel().update(user_group, updates) |
|
794 | UserGroupModel().update(user_group, updates) | |
799 | Session().commit() |
|
795 | meta.Session().commit() | |
800 | return dict( |
|
796 | return dict( | |
801 | msg='updated user group ID:%s %s' % (user_group.users_group_id, |
|
797 | msg='updated user group ID:%s %s' % (user_group.users_group_id, | |
802 | user_group.users_group_name), |
|
798 | user_group.users_group_name), | |
@@ -842,7 +838,7 b' class ApiController(JSONRPCController):' | |||||
842 |
|
838 | |||
843 | try: |
|
839 | try: | |
844 | UserGroupModel().delete(user_group) |
|
840 | UserGroupModel().delete(user_group) | |
845 | Session().commit() |
|
841 | meta.Session().commit() | |
846 | return dict( |
|
842 | return dict( | |
847 | msg='deleted user group ID:%s %s' % |
|
843 | msg='deleted user group ID:%s %s' % | |
848 | (user_group.users_group_id, user_group.users_group_name), |
|
844 | (user_group.users_group_id, user_group.users_group_name), | |
@@ -903,7 +899,7 b' class ApiController(JSONRPCController):' | |||||
903 | user.username, user_group.users_group_name |
|
899 | user.username, user_group.users_group_name | |
904 | ) |
|
900 | ) | |
905 | msg = msg if success else 'User is already in that group' |
|
901 | msg = msg if success else 'User is already in that group' | |
906 | Session().commit() |
|
902 | meta.Session().commit() | |
907 |
|
903 | |||
908 | return dict( |
|
904 | return dict( | |
909 | success=success, |
|
905 | success=success, | |
@@ -951,7 +947,7 b' class ApiController(JSONRPCController):' | |||||
951 | user.username, user_group.users_group_name |
|
947 | user.username, user_group.users_group_name | |
952 | ) |
|
948 | ) | |
953 | msg = msg if success else "User wasn't in group" |
|
949 | msg = msg if success else "User wasn't in group" | |
954 | Session().commit() |
|
950 | meta.Session().commit() | |
955 | return dict(success=success, msg=msg) |
|
951 | return dict(success=success, msg=msg) | |
956 | except Exception: |
|
952 | except Exception: | |
957 | log.error(traceback.format_exc()) |
|
953 | log.error(traceback.format_exc()) | |
@@ -963,8 +959,8 b' class ApiController(JSONRPCController):' | |||||
963 |
|
959 | |||
964 | # permission check inside |
|
960 | # permission check inside | |
965 | def get_repo(self, repoid, |
|
961 | def get_repo(self, repoid, | |
966 |
with_revision_names= |
|
962 | with_revision_names=False, | |
967 |
with_pullrequests= |
|
963 | with_pullrequests=False): | |
968 | """ |
|
964 | """ | |
969 | Gets an existing repository by it's name or repository_id. Members will return |
|
965 | Gets an existing repository by it's name or repository_id. Members will return | |
970 | either users_group or user associated to that repository. This command can be |
|
966 | either users_group or user associated to that repository. This command can be | |
@@ -1064,8 +1060,8 b' class ApiController(JSONRPCController):' | |||||
1064 | for uf in repo.followers |
|
1060 | for uf in repo.followers | |
1065 | ] |
|
1061 | ] | |
1066 |
|
1062 | |||
1067 |
data = repo.get_api_data(with_revision_names= |
|
1063 | data = repo.get_api_data(with_revision_names=with_revision_names, | |
1068 |
with_pullrequests= |
|
1064 | with_pullrequests=with_pullrequests) | |
1069 | data['members'] = members |
|
1065 | data['members'] = members | |
1070 | data['followers'] = followers |
|
1066 | data['followers'] = followers | |
1071 | return data |
|
1067 | return data | |
@@ -1101,9 +1097,9 b' class ApiController(JSONRPCController):' | |||||
1101 | error: null |
|
1097 | error: null | |
1102 | """ |
|
1098 | """ | |
1103 | if not HasPermissionAny('hg.admin')(): |
|
1099 | if not HasPermissionAny('hg.admin')(): | |
1104 |
repos = |
|
1100 | repos = request.authuser.get_all_user_repos() | |
1105 | else: |
|
1101 | else: | |
1106 | repos = Repository.query() |
|
1102 | repos = db.Repository.query() | |
1107 |
|
1103 | |||
1108 | return [ |
|
1104 | return [ | |
1109 | repo.get_api_data() |
|
1105 | repo.get_api_data() | |
@@ -1112,7 +1108,7 b' class ApiController(JSONRPCController):' | |||||
1112 |
|
1108 | |||
1113 | # permission check inside |
|
1109 | # permission check inside | |
1114 | def get_repo_nodes(self, repoid, revision, root_path, |
|
1110 | def get_repo_nodes(self, repoid, revision, root_path, | |
1115 |
ret_type= |
|
1111 | ret_type='all'): | |
1116 | """ |
|
1112 | """ | |
1117 | returns a list of nodes and it's children in a flat list for a given path |
|
1113 | returns a list of nodes and it's children in a flat list for a given path | |
1118 | at given revision. It's possible to specify ret_type to show only `files` or |
|
1114 | at given revision. It's possible to specify ret_type to show only `files` or | |
@@ -1147,7 +1143,6 b' class ApiController(JSONRPCController):' | |||||
1147 | if not HasRepoPermissionLevel('read')(repo.repo_name): |
|
1143 | if not HasRepoPermissionLevel('read')(repo.repo_name): | |
1148 | raise JSONRPCError('repository `%s` does not exist' % (repoid,)) |
|
1144 | raise JSONRPCError('repository `%s` does not exist' % (repoid,)) | |
1149 |
|
1145 | |||
1150 | ret_type = Optional.extract(ret_type) |
|
|||
1151 | _map = {} |
|
1146 | _map = {} | |
1152 | try: |
|
1147 | try: | |
1153 | _d, _f = ScmModel().get_nodes(repo, revision, root_path, |
|
1148 | _d, _f = ScmModel().get_nodes(repo, revision, root_path, | |
@@ -1168,13 +1163,13 b' class ApiController(JSONRPCController):' | |||||
1168 | ) |
|
1163 | ) | |
1169 |
|
1164 | |||
1170 | # permission check inside |
|
1165 | # permission check inside | |
1171 |
def create_repo(self, repo_name, owner= |
|
1166 | def create_repo(self, repo_name, owner=None, | |
1172 |
repo_type= |
|
1167 | repo_type=None, description='', | |
1173 |
private= |
|
1168 | private=False, clone_uri=None, | |
1174 |
landing_rev= |
|
1169 | landing_rev='rev:tip', | |
1175 |
enable_statistics= |
|
1170 | enable_statistics=None, | |
1176 |
enable_downloads= |
|
1171 | enable_downloads=None, | |
1177 |
copy_permissions= |
|
1172 | copy_permissions=False): | |
1178 | """ |
|
1173 | """ | |
1179 | Creates a repository. The repository name contains the full path, but the |
|
1174 | Creates a repository. The repository name contains the full path, but the | |
1180 | parent repository group must exist. For example "foo/bar/baz" require the groups |
|
1175 | parent repository group must exist. For example "foo/bar/baz" require the groups | |
@@ -1228,7 +1223,7 b' class ApiController(JSONRPCController):' | |||||
1228 | repo_name_parts = repo_name.split('/') |
|
1223 | repo_name_parts = repo_name.split('/') | |
1229 | if len(repo_name_parts) > 1: |
|
1224 | if len(repo_name_parts) > 1: | |
1230 | group_name = '/'.join(repo_name_parts[:-1]) |
|
1225 | group_name = '/'.join(repo_name_parts[:-1]) | |
1231 | repo_group = RepoGroup.get_by_group_name(group_name) |
|
1226 | repo_group = db.RepoGroup.get_by_group_name(group_name) | |
1232 | if repo_group is None: |
|
1227 | if repo_group is None: | |
1233 | raise JSONRPCError("repo group `%s` not found" % group_name) |
|
1228 | raise JSONRPCError("repo group `%s` not found" % group_name) | |
1234 | if not(HasPermissionAny('hg.admin')() or HasRepoGroupPermissionLevel('write')(group_name)): |
|
1229 | if not(HasPermissionAny('hg.admin')() or HasRepoGroupPermissionLevel('write')(group_name)): | |
@@ -1238,12 +1233,12 b' class ApiController(JSONRPCController):' | |||||
1238 | raise JSONRPCError("no permission to create top level repo") |
|
1233 | raise JSONRPCError("no permission to create top level repo") | |
1239 |
|
1234 | |||
1240 | if not HasPermissionAny('hg.admin')(): |
|
1235 | if not HasPermissionAny('hg.admin')(): | |
1241 |
if not |
|
1236 | if owner is not None: | |
1242 | # forbid setting owner for non-admins |
|
1237 | # forbid setting owner for non-admins | |
1243 | raise JSONRPCError( |
|
1238 | raise JSONRPCError( | |
1244 | 'Only Kallithea admin can specify `owner` param' |
|
1239 | 'Only Kallithea admin can specify `owner` param' | |
1245 | ) |
|
1240 | ) | |
1246 |
if |
|
1241 | if owner is None: | |
1247 | owner = request.authuser.user_id |
|
1242 | owner = request.authuser.user_id | |
1248 |
|
1243 | |||
1249 | owner = get_user_or_error(owner) |
|
1244 | owner = get_user_or_error(owner) | |
@@ -1251,28 +1246,22 b' class ApiController(JSONRPCController):' | |||||
1251 | if RepoModel().get_by_repo_name(repo_name): |
|
1246 | if RepoModel().get_by_repo_name(repo_name): | |
1252 | raise JSONRPCError("repo `%s` already exist" % repo_name) |
|
1247 | raise JSONRPCError("repo `%s` already exist" % repo_name) | |
1253 |
|
1248 | |||
1254 | defs = Setting.get_default_repo_settings(strip_prefix=True) |
|
1249 | defs = db.Setting.get_default_repo_settings(strip_prefix=True) | |
1255 |
if |
|
1250 | if private is None: | |
1256 |
private = defs.get('repo_private') or |
|
1251 | private = defs.get('repo_private') or False | |
1257 |
if |
|
1252 | if repo_type is None: | |
1258 | repo_type = defs.get('repo_type') |
|
1253 | repo_type = defs.get('repo_type') | |
1259 |
if |
|
1254 | if enable_statistics is None: | |
1260 | enable_statistics = defs.get('repo_enable_statistics') |
|
1255 | enable_statistics = defs.get('repo_enable_statistics') | |
1261 |
if |
|
1256 | if enable_downloads is None: | |
1262 | enable_downloads = defs.get('repo_enable_downloads') |
|
1257 | enable_downloads = defs.get('repo_enable_downloads') | |
1263 |
|
1258 | |||
1264 | clone_uri = Optional.extract(clone_uri) |
|
|||
1265 | description = Optional.extract(description) |
|
|||
1266 | landing_rev = Optional.extract(landing_rev) |
|
|||
1267 | copy_permissions = Optional.extract(copy_permissions) |
|
|||
1268 |
|
||||
1269 | try: |
|
1259 | try: | |
1270 | data = dict( |
|
1260 | data = dict( | |
1271 | repo_name=repo_name_parts[-1], |
|
1261 | repo_name=repo_name_parts[-1], | |
1272 | repo_name_full=repo_name, |
|
1262 | repo_name_full=repo_name, | |
1273 | repo_type=repo_type, |
|
1263 | repo_type=repo_type, | |
1274 | repo_description=description, |
|
1264 | repo_description=description, | |
1275 | owner=owner, |
|
|||
1276 | repo_private=private, |
|
1265 | repo_private=private, | |
1277 | clone_uri=clone_uri, |
|
1266 | clone_uri=clone_uri, | |
1278 | repo_group=group_name, |
|
1267 | repo_group=group_name, | |
@@ -1282,14 +1271,12 b' class ApiController(JSONRPCController):' | |||||
1282 | repo_copy_permissions=copy_permissions, |
|
1271 | repo_copy_permissions=copy_permissions, | |
1283 | ) |
|
1272 | ) | |
1284 |
|
1273 | |||
1285 |
|
|
1274 | RepoModel().create(form_data=data, cur_user=owner.username) | |
1286 | task_id = task.task_id |
|
|||
1287 | # no commit, it's done in RepoModel, or async via celery |
|
1275 | # no commit, it's done in RepoModel, or async via celery | |
1288 | return dict( |
|
1276 | return dict( | |
1289 | msg="Created new repository `%s`" % (repo_name,), |
|
1277 | msg="Created new repository `%s`" % (repo_name,), | |
1290 | success=True, # cannot return the repo data here since fork |
|
1278 | success=True, # cannot return the repo data here since fork | |
1291 | # can be done async |
|
1279 | # can be done async | |
1292 | task=task_id |
|
|||
1293 | ) |
|
1280 | ) | |
1294 | except Exception: |
|
1281 | except Exception: | |
1295 | log.error(traceback.format_exc()) |
|
1282 | log.error(traceback.format_exc()) | |
@@ -1297,13 +1284,13 b' class ApiController(JSONRPCController):' | |||||
1297 | 'failed to create repository `%s`' % (repo_name,)) |
|
1284 | 'failed to create repository `%s`' % (repo_name,)) | |
1298 |
|
1285 | |||
1299 | # permission check inside |
|
1286 | # permission check inside | |
1300 |
def update_repo(self, repoid, name= |
|
1287 | def update_repo(self, repoid, name=None, | |
1301 |
owner= |
|
1288 | owner=None, | |
1302 |
group= |
|
1289 | group=None, | |
1303 |
description= |
|
1290 | description=None, private=None, | |
1304 |
clone_uri= |
|
1291 | clone_uri=None, landing_rev=None, | |
1305 |
enable_statistics= |
|
1292 | enable_statistics=None, | |
1306 |
enable_downloads= |
|
1293 | enable_downloads=None): | |
1307 |
|
1294 | |||
1308 | """ |
|
1295 | """ | |
1309 | Updates repo |
|
1296 | Updates repo | |
@@ -1330,7 +1317,7 b' class ApiController(JSONRPCController):' | |||||
1330 | ): |
|
1317 | ): | |
1331 | raise JSONRPCError('no permission to create (or move) top level repositories') |
|
1318 | raise JSONRPCError('no permission to create (or move) top level repositories') | |
1332 |
|
1319 | |||
1333 |
if not |
|
1320 | if owner is not None: | |
1334 | # forbid setting owner for non-admins |
|
1321 | # forbid setting owner for non-admins | |
1335 | raise JSONRPCError( |
|
1322 | raise JSONRPCError( | |
1336 | 'Only Kallithea admin can specify `owner` param' |
|
1323 | 'Only Kallithea admin can specify `owner` param' | |
@@ -1338,7 +1325,7 b' class ApiController(JSONRPCController):' | |||||
1338 |
|
1325 | |||
1339 | updates = {} |
|
1326 | updates = {} | |
1340 | repo_group = group |
|
1327 | repo_group = group | |
1341 |
if not |
|
1328 | if repo_group is not None: | |
1342 | repo_group = get_repo_group_or_error(repo_group) # TODO: repos can thus currently not be moved to root |
|
1329 | repo_group = get_repo_group_or_error(repo_group) # TODO: repos can thus currently not be moved to root | |
1343 | if repo_group.group_id != repo.group_id: |
|
1330 | if repo_group.group_id != repo.group_id: | |
1344 | if not(HasPermissionAny('hg.admin')() or HasRepoGroupPermissionLevel('write')(repo_group.group_name)): |
|
1331 | if not(HasPermissionAny('hg.admin')() or HasRepoGroupPermissionLevel('write')(repo_group.group_name)): | |
@@ -1356,7 +1343,7 b' class ApiController(JSONRPCController):' | |||||
1356 | store_update(updates, enable_downloads, 'repo_enable_downloads') |
|
1343 | store_update(updates, enable_downloads, 'repo_enable_downloads') | |
1357 |
|
1344 | |||
1358 | RepoModel().update(repo, **updates) |
|
1345 | RepoModel().update(repo, **updates) | |
1359 | Session().commit() |
|
1346 | meta.Session().commit() | |
1360 | return dict( |
|
1347 | return dict( | |
1361 | msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), |
|
1348 | msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), | |
1362 | repository=repo.get_api_data() |
|
1349 | repository=repo.get_api_data() | |
@@ -1368,9 +1355,9 b' class ApiController(JSONRPCController):' | |||||
1368 | # permission check inside |
|
1355 | # permission check inside | |
1369 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') |
|
1356 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') | |
1370 | def fork_repo(self, repoid, fork_name, |
|
1357 | def fork_repo(self, repoid, fork_name, | |
1371 |
owner= |
|
1358 | owner=None, | |
1372 |
description= |
|
1359 | description='', copy_permissions=False, | |
1373 |
private= |
|
1360 | private=False, landing_rev='rev:tip'): | |
1374 | """ |
|
1361 | """ | |
1375 | Creates a fork of given repo. In case of using celery this will |
|
1362 | Creates a fork of given repo. In case of using celery this will | |
1376 | immediately return success message, while fork is going to be created |
|
1363 | immediately return success message, while fork is going to be created | |
@@ -1424,7 +1411,7 b' class ApiController(JSONRPCController):' | |||||
1424 | fork_name_parts = fork_name.split('/') |
|
1411 | fork_name_parts = fork_name.split('/') | |
1425 | if len(fork_name_parts) > 1: |
|
1412 | if len(fork_name_parts) > 1: | |
1426 | group_name = '/'.join(fork_name_parts[:-1]) |
|
1413 | group_name = '/'.join(fork_name_parts[:-1]) | |
1427 | repo_group = RepoGroup.get_by_group_name(group_name) |
|
1414 | repo_group = db.RepoGroup.get_by_group_name(group_name) | |
1428 | if repo_group is None: |
|
1415 | if repo_group is None: | |
1429 | raise JSONRPCError("repo group `%s` not found" % group_name) |
|
1416 | raise JSONRPCError("repo group `%s` not found" % group_name) | |
1430 | if not(HasPermissionAny('hg.admin')() or HasRepoGroupPermissionLevel('write')(group_name)): |
|
1417 | if not(HasPermissionAny('hg.admin')() or HasRepoGroupPermissionLevel('write')(group_name)): | |
@@ -1436,7 +1423,7 b' class ApiController(JSONRPCController):' | |||||
1436 | if HasPermissionAny('hg.admin')(): |
|
1423 | if HasPermissionAny('hg.admin')(): | |
1437 | pass |
|
1424 | pass | |
1438 | elif HasRepoPermissionLevel('read')(repo.repo_name): |
|
1425 | elif HasRepoPermissionLevel('read')(repo.repo_name): | |
1439 |
if not |
|
1426 | if owner is not None: | |
1440 | # forbid setting owner for non-admins |
|
1427 | # forbid setting owner for non-admins | |
1441 | raise JSONRPCError( |
|
1428 | raise JSONRPCError( | |
1442 | 'Only Kallithea admin can specify `owner` param' |
|
1429 | 'Only Kallithea admin can specify `owner` param' | |
@@ -1444,7 +1431,7 b' class ApiController(JSONRPCController):' | |||||
1444 | else: |
|
1431 | else: | |
1445 | raise JSONRPCError('repository `%s` does not exist' % (repoid,)) |
|
1432 | raise JSONRPCError('repository `%s` does not exist' % (repoid,)) | |
1446 |
|
1433 | |||
1447 |
if |
|
1434 | if owner is None: | |
1448 | owner = request.authuser.user_id |
|
1435 | owner = request.authuser.user_id | |
1449 |
|
1436 | |||
1450 | owner = get_user_or_error(owner) |
|
1437 | owner = get_user_or_error(owner) | |
@@ -1455,22 +1442,20 b' class ApiController(JSONRPCController):' | |||||
1455 | repo_name_full=fork_name, |
|
1442 | repo_name_full=fork_name, | |
1456 | repo_group=group_name, |
|
1443 | repo_group=group_name, | |
1457 | repo_type=repo.repo_type, |
|
1444 | repo_type=repo.repo_type, | |
1458 |
description= |
|
1445 | description=description, | |
1459 |
private= |
|
1446 | private=private, | |
1460 |
copy_permissions= |
|
1447 | copy_permissions=copy_permissions, | |
1461 |
landing_rev= |
|
1448 | landing_rev=landing_rev, | |
1462 | update_after_clone=False, |
|
1449 | update_after_clone=False, | |
1463 | fork_parent_id=repo.repo_id, |
|
1450 | fork_parent_id=repo.repo_id, | |
1464 | ) |
|
1451 | ) | |
1465 |
|
|
1452 | RepoModel().create_fork(form_data, cur_user=owner.username) | |
1466 | # no commit, it's done in RepoModel, or async via celery |
|
1453 | # no commit, it's done in RepoModel, or async via celery | |
1467 | task_id = task.task_id |
|
|||
1468 | return dict( |
|
1454 | return dict( | |
1469 | msg='Created fork of `%s` as `%s`' % (repo.repo_name, |
|
1455 | msg='Created fork of `%s` as `%s`' % (repo.repo_name, | |
1470 | fork_name), |
|
1456 | fork_name), | |
1471 | success=True, # cannot return the repo data here since fork |
|
1457 | success=True, # cannot return the repo data here since fork | |
1472 | # can be done async |
|
1458 | # can be done async | |
1473 | task=task_id |
|
|||
1474 | ) |
|
1459 | ) | |
1475 | except Exception: |
|
1460 | except Exception: | |
1476 | log.error(traceback.format_exc()) |
|
1461 | log.error(traceback.format_exc()) | |
@@ -1480,7 +1465,7 b' class ApiController(JSONRPCController):' | |||||
1480 | ) |
|
1465 | ) | |
1481 |
|
1466 | |||
1482 | # permission check inside |
|
1467 | # permission check inside | |
1483 |
def delete_repo(self, repoid, forks= |
|
1468 | def delete_repo(self, repoid, forks=''): | |
1484 | """ |
|
1469 | """ | |
1485 | Deletes a repository. This command can be executed only using api_key belonging |
|
1470 | Deletes a repository. This command can be executed only using api_key belonging | |
1486 | to user with admin rights or regular user that have admin access to repository. |
|
1471 | to user with admin rights or regular user that have admin access to repository. | |
@@ -1509,7 +1494,7 b' class ApiController(JSONRPCController):' | |||||
1509 | raise JSONRPCError('repository `%s` does not exist' % (repoid,)) |
|
1494 | raise JSONRPCError('repository `%s` does not exist' % (repoid,)) | |
1510 |
|
1495 | |||
1511 | try: |
|
1496 | try: | |
1512 |
handle_forks = |
|
1497 | handle_forks = forks | |
1513 | _forks_msg = '' |
|
1498 | _forks_msg = '' | |
1514 | _forks = [f for f in repo.forks] |
|
1499 | _forks = [f for f in repo.forks] | |
1515 | if handle_forks == 'detach': |
|
1500 | if handle_forks == 'detach': | |
@@ -1523,7 +1508,7 b' class ApiController(JSONRPCController):' | |||||
1523 | ) |
|
1508 | ) | |
1524 |
|
1509 | |||
1525 | RepoModel().delete(repo, forks=forks) |
|
1510 | RepoModel().delete(repo, forks=forks) | |
1526 | Session().commit() |
|
1511 | meta.Session().commit() | |
1527 | return dict( |
|
1512 | return dict( | |
1528 | msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg), |
|
1513 | msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg), | |
1529 | success=True |
|
1514 | success=True | |
@@ -1564,7 +1549,7 b' class ApiController(JSONRPCController):' | |||||
1564 |
|
1549 | |||
1565 | RepoModel().grant_user_permission(repo=repo, user=user, perm=perm) |
|
1550 | RepoModel().grant_user_permission(repo=repo, user=user, perm=perm) | |
1566 |
|
1551 | |||
1567 | Session().commit() |
|
1552 | meta.Session().commit() | |
1568 | return dict( |
|
1553 | return dict( | |
1569 | msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % ( |
|
1554 | msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % ( | |
1570 | perm.permission_name, user.username, repo.repo_name |
|
1555 | perm.permission_name, user.username, repo.repo_name | |
@@ -1604,7 +1589,7 b' class ApiController(JSONRPCController):' | |||||
1604 | user = get_user_or_error(userid) |
|
1589 | user = get_user_or_error(userid) | |
1605 | try: |
|
1590 | try: | |
1606 | RepoModel().revoke_user_permission(repo=repo, user=user) |
|
1591 | RepoModel().revoke_user_permission(repo=repo, user=user) | |
1607 | Session().commit() |
|
1592 | meta.Session().commit() | |
1608 | return dict( |
|
1593 | return dict( | |
1609 | msg='Revoked perm for user: `%s` in repo: `%s`' % ( |
|
1594 | msg='Revoked perm for user: `%s` in repo: `%s`' % ( | |
1610 | user.username, repo.repo_name |
|
1595 | user.username, repo.repo_name | |
@@ -1666,7 +1651,7 b' class ApiController(JSONRPCController):' | |||||
1666 | RepoModel().grant_user_group_permission( |
|
1651 | RepoModel().grant_user_group_permission( | |
1667 | repo=repo, group_name=user_group, perm=perm) |
|
1652 | repo=repo, group_name=user_group, perm=perm) | |
1668 |
|
1653 | |||
1669 | Session().commit() |
|
1654 | meta.Session().commit() | |
1670 | return dict( |
|
1655 | return dict( | |
1671 | msg='Granted perm: `%s` for user group: `%s` in ' |
|
1656 | msg='Granted perm: `%s` for user group: `%s` in ' | |
1672 | 'repo: `%s`' % ( |
|
1657 | 'repo: `%s`' % ( | |
@@ -1716,7 +1701,7 b' class ApiController(JSONRPCController):' | |||||
1716 | RepoModel().revoke_user_group_permission( |
|
1701 | RepoModel().revoke_user_group_permission( | |
1717 | repo=repo, group_name=user_group) |
|
1702 | repo=repo, group_name=user_group) | |
1718 |
|
1703 | |||
1719 | Session().commit() |
|
1704 | meta.Session().commit() | |
1720 | return dict( |
|
1705 | return dict( | |
1721 | msg='Revoked perm for user group: `%s` in repo: `%s`' % ( |
|
1706 | msg='Revoked perm for user group: `%s` in repo: `%s`' % ( | |
1722 | user_group.users_group_name, repo.repo_name |
|
1707 | user_group.users_group_name, repo.repo_name | |
@@ -1776,14 +1761,14 b' class ApiController(JSONRPCController):' | |||||
1776 | """ |
|
1761 | """ | |
1777 | return [ |
|
1762 | return [ | |
1778 | repo_group.get_api_data() |
|
1763 | repo_group.get_api_data() | |
1779 | for repo_group in RepoGroup.query() |
|
1764 | for repo_group in db.RepoGroup.query() | |
1780 | ] |
|
1765 | ] | |
1781 |
|
1766 | |||
1782 | @HasPermissionAnyDecorator('hg.admin') |
|
1767 | @HasPermissionAnyDecorator('hg.admin') | |
1783 |
def create_repo_group(self, group_name, description= |
|
1768 | def create_repo_group(self, group_name, description='', | |
1784 |
owner= |
|
1769 | owner=None, | |
1785 |
parent= |
|
1770 | parent=None, | |
1786 |
copy_permissions= |
|
1771 | copy_permissions=False): | |
1787 | """ |
|
1772 | """ | |
1788 | Creates a repository group. This command can be executed only using |
|
1773 | Creates a repository group. This command can be executed only using | |
1789 | api_key belonging to user with admin rights. |
|
1774 | api_key belonging to user with admin rights. | |
@@ -1817,17 +1802,16 b' class ApiController(JSONRPCController):' | |||||
1817 | } |
|
1802 | } | |
1818 |
|
1803 | |||
1819 | """ |
|
1804 | """ | |
1820 | if RepoGroup.get_by_group_name(group_name): |
|
1805 | if db.RepoGroup.get_by_group_name(group_name): | |
1821 | raise JSONRPCError("repo group `%s` already exist" % (group_name,)) |
|
1806 | raise JSONRPCError("repo group `%s` already exist" % (group_name,)) | |
1822 |
|
1807 | |||
1823 |
if |
|
1808 | if owner is None: | |
1824 | owner = request.authuser.user_id |
|
1809 | owner = request.authuser.user_id | |
1825 |
group_description = |
|
1810 | group_description = description | |
1826 |
parent_group = |
|
1811 | parent_group = None | |
1827 | if not isinstance(parent, Optional): |
|
1812 | if parent is not None: | |
1828 |
parent_group = get_repo_group_or_error(parent |
|
1813 | parent_group = get_repo_group_or_error(parent) | |
1829 |
|
1814 | |||
1830 | copy_permissions = Optional.extract(copy_permissions) |
|
|||
1831 | try: |
|
1815 | try: | |
1832 | repo_group = RepoGroupModel().create( |
|
1816 | repo_group = RepoGroupModel().create( | |
1833 | group_name=group_name, |
|
1817 | group_name=group_name, | |
@@ -1836,7 +1820,7 b' class ApiController(JSONRPCController):' | |||||
1836 | parent=parent_group, |
|
1820 | parent=parent_group, | |
1837 | copy_permissions=copy_permissions |
|
1821 | copy_permissions=copy_permissions | |
1838 | ) |
|
1822 | ) | |
1839 | Session().commit() |
|
1823 | meta.Session().commit() | |
1840 | return dict( |
|
1824 | return dict( | |
1841 | msg='created new repo group `%s`' % group_name, |
|
1825 | msg='created new repo group `%s`' % group_name, | |
1842 | repo_group=repo_group.get_api_data() |
|
1826 | repo_group=repo_group.get_api_data() | |
@@ -1847,10 +1831,10 b' class ApiController(JSONRPCController):' | |||||
1847 | raise JSONRPCError('failed to create repo group `%s`' % (group_name,)) |
|
1831 | raise JSONRPCError('failed to create repo group `%s`' % (group_name,)) | |
1848 |
|
1832 | |||
1849 | @HasPermissionAnyDecorator('hg.admin') |
|
1833 | @HasPermissionAnyDecorator('hg.admin') | |
1850 |
def update_repo_group(self, repogroupid, group_name= |
|
1834 | def update_repo_group(self, repogroupid, group_name=None, | |
1851 |
description= |
|
1835 | description=None, | |
1852 |
owner= |
|
1836 | owner=None, | |
1853 |
parent= |
|
1837 | parent=None): | |
1854 | repo_group = get_repo_group_or_error(repogroupid) |
|
1838 | repo_group = get_repo_group_or_error(repogroupid) | |
1855 |
|
1839 | |||
1856 | updates = {} |
|
1840 | updates = {} | |
@@ -1860,7 +1844,7 b' class ApiController(JSONRPCController):' | |||||
1860 | store_update(updates, owner, 'owner') |
|
1844 | store_update(updates, owner, 'owner') | |
1861 | store_update(updates, parent, 'parent_group') |
|
1845 | store_update(updates, parent, 'parent_group') | |
1862 | repo_group = RepoGroupModel().update(repo_group, updates) |
|
1846 | repo_group = RepoGroupModel().update(repo_group, updates) | |
1863 | Session().commit() |
|
1847 | meta.Session().commit() | |
1864 | return dict( |
|
1848 | return dict( | |
1865 | msg='updated repository group ID:%s %s' % (repo_group.group_id, |
|
1849 | msg='updated repository group ID:%s %s' % (repo_group.group_id, | |
1866 | repo_group.group_name), |
|
1850 | repo_group.group_name), | |
@@ -1900,7 +1884,7 b' class ApiController(JSONRPCController):' | |||||
1900 |
|
1884 | |||
1901 | try: |
|
1885 | try: | |
1902 | RepoGroupModel().delete(repo_group) |
|
1886 | RepoGroupModel().delete(repo_group) | |
1903 | Session().commit() |
|
1887 | meta.Session().commit() | |
1904 | return dict( |
|
1888 | return dict( | |
1905 | msg='deleted repo group ID:%s %s' % |
|
1889 | msg='deleted repo group ID:%s %s' % | |
1906 | (repo_group.group_id, repo_group.group_name), |
|
1890 | (repo_group.group_id, repo_group.group_name), | |
@@ -1914,7 +1898,7 b' class ApiController(JSONRPCController):' | |||||
1914 |
|
1898 | |||
1915 | # permission check inside |
|
1899 | # permission check inside | |
1916 | def grant_user_permission_to_repo_group(self, repogroupid, userid, |
|
1900 | def grant_user_permission_to_repo_group(self, repogroupid, userid, | |
1917 |
perm, apply_to_children= |
|
1901 | perm, apply_to_children='none'): | |
1918 | """ |
|
1902 | """ | |
1919 | Grant permission for user on given repository group, or update existing |
|
1903 | Grant permission for user on given repository group, or update existing | |
1920 | one if found. This command can be executed only using api_key belonging |
|
1904 | one if found. This command can be executed only using api_key belonging | |
@@ -1956,7 +1940,6 b' class ApiController(JSONRPCController):' | |||||
1956 |
|
1940 | |||
1957 | user = get_user_or_error(userid) |
|
1941 | user = get_user_or_error(userid) | |
1958 | perm = get_perm_or_error(perm, prefix='group.') |
|
1942 | perm = get_perm_or_error(perm, prefix='group.') | |
1959 | apply_to_children = Optional.extract(apply_to_children) |
|
|||
1960 |
|
1943 | |||
1961 | try: |
|
1944 | try: | |
1962 | RepoGroupModel().add_permission(repo_group=repo_group, |
|
1945 | RepoGroupModel().add_permission(repo_group=repo_group, | |
@@ -1964,7 +1947,7 b' class ApiController(JSONRPCController):' | |||||
1964 | obj_type="user", |
|
1947 | obj_type="user", | |
1965 | perm=perm, |
|
1948 | perm=perm, | |
1966 | recursive=apply_to_children) |
|
1949 | recursive=apply_to_children) | |
1967 | Session().commit() |
|
1950 | meta.Session().commit() | |
1968 | return dict( |
|
1951 | return dict( | |
1969 | msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % ( |
|
1952 | msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % ( | |
1970 | perm.permission_name, apply_to_children, user.username, repo_group.name |
|
1953 | perm.permission_name, apply_to_children, user.username, repo_group.name | |
@@ -1979,7 +1962,7 b' class ApiController(JSONRPCController):' | |||||
1979 |
|
1962 | |||
1980 | # permission check inside |
|
1963 | # permission check inside | |
1981 | def revoke_user_permission_from_repo_group(self, repogroupid, userid, |
|
1964 | def revoke_user_permission_from_repo_group(self, repogroupid, userid, | |
1982 |
apply_to_children= |
|
1965 | apply_to_children='none'): | |
1983 | """ |
|
1966 | """ | |
1984 | Revoke permission for user on given repository group. This command can |
|
1967 | Revoke permission for user on given repository group. This command can | |
1985 | be executed only using api_key belonging to user with admin rights, or |
|
1968 | be executed only using api_key belonging to user with admin rights, or | |
@@ -2018,7 +2001,6 b' class ApiController(JSONRPCController):' | |||||
2018 | raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,)) |
|
2001 | raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,)) | |
2019 |
|
2002 | |||
2020 | user = get_user_or_error(userid) |
|
2003 | user = get_user_or_error(userid) | |
2021 | apply_to_children = Optional.extract(apply_to_children) |
|
|||
2022 |
|
2004 | |||
2023 | try: |
|
2005 | try: | |
2024 | RepoGroupModel().delete_permission(repo_group=repo_group, |
|
2006 | RepoGroupModel().delete_permission(repo_group=repo_group, | |
@@ -2026,7 +2008,7 b' class ApiController(JSONRPCController):' | |||||
2026 | obj_type="user", |
|
2008 | obj_type="user", | |
2027 | recursive=apply_to_children) |
|
2009 | recursive=apply_to_children) | |
2028 |
|
2010 | |||
2029 | Session().commit() |
|
2011 | meta.Session().commit() | |
2030 | return dict( |
|
2012 | return dict( | |
2031 | msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % ( |
|
2013 | msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % ( | |
2032 | apply_to_children, user.username, repo_group.name |
|
2014 | apply_to_children, user.username, repo_group.name | |
@@ -2042,7 +2024,7 b' class ApiController(JSONRPCController):' | |||||
2042 | # permission check inside |
|
2024 | # permission check inside | |
2043 | def grant_user_group_permission_to_repo_group( |
|
2025 | def grant_user_group_permission_to_repo_group( | |
2044 | self, repogroupid, usergroupid, perm, |
|
2026 | self, repogroupid, usergroupid, perm, | |
2045 |
apply_to_children= |
|
2027 | apply_to_children='none'): | |
2046 | """ |
|
2028 | """ | |
2047 | Grant permission for user group on given repository group, or update |
|
2029 | Grant permission for user group on given repository group, or update | |
2048 | existing one if found. This command can be executed only using |
|
2030 | existing one if found. This command can be executed only using | |
@@ -2089,15 +2071,13 b' class ApiController(JSONRPCController):' | |||||
2089 | raise JSONRPCError( |
|
2071 | raise JSONRPCError( | |
2090 | 'user group `%s` does not exist' % (usergroupid,)) |
|
2072 | 'user group `%s` does not exist' % (usergroupid,)) | |
2091 |
|
2073 | |||
2092 | apply_to_children = Optional.extract(apply_to_children) |
|
|||
2093 |
|
||||
2094 | try: |
|
2074 | try: | |
2095 | RepoGroupModel().add_permission(repo_group=repo_group, |
|
2075 | RepoGroupModel().add_permission(repo_group=repo_group, | |
2096 | obj=user_group, |
|
2076 | obj=user_group, | |
2097 | obj_type="user_group", |
|
2077 | obj_type="user_group", | |
2098 | perm=perm, |
|
2078 | perm=perm, | |
2099 | recursive=apply_to_children) |
|
2079 | recursive=apply_to_children) | |
2100 | Session().commit() |
|
2080 | meta.Session().commit() | |
2101 | return dict( |
|
2081 | return dict( | |
2102 | msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % ( |
|
2082 | msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % ( | |
2103 | perm.permission_name, apply_to_children, |
|
2083 | perm.permission_name, apply_to_children, | |
@@ -2117,7 +2097,7 b' class ApiController(JSONRPCController):' | |||||
2117 | # permission check inside |
|
2097 | # permission check inside | |
2118 | def revoke_user_group_permission_from_repo_group( |
|
2098 | def revoke_user_group_permission_from_repo_group( | |
2119 | self, repogroupid, usergroupid, |
|
2099 | self, repogroupid, usergroupid, | |
2120 |
apply_to_children= |
|
2100 | apply_to_children='none'): | |
2121 | """ |
|
2101 | """ | |
2122 | Revoke permission for user group on given repository. This command can be |
|
2102 | Revoke permission for user group on given repository. This command can be | |
2123 | executed only using api_key belonging to user with admin rights, or |
|
2103 | executed only using api_key belonging to user with admin rights, or | |
@@ -2159,14 +2139,12 b' class ApiController(JSONRPCController):' | |||||
2159 | raise JSONRPCError( |
|
2139 | raise JSONRPCError( | |
2160 | 'user group `%s` does not exist' % (usergroupid,)) |
|
2140 | 'user group `%s` does not exist' % (usergroupid,)) | |
2161 |
|
2141 | |||
2162 | apply_to_children = Optional.extract(apply_to_children) |
|
|||
2163 |
|
||||
2164 | try: |
|
2142 | try: | |
2165 | RepoGroupModel().delete_permission(repo_group=repo_group, |
|
2143 | RepoGroupModel().delete_permission(repo_group=repo_group, | |
2166 | obj=user_group, |
|
2144 | obj=user_group, | |
2167 | obj_type="user_group", |
|
2145 | obj_type="user_group", | |
2168 | recursive=apply_to_children) |
|
2146 | recursive=apply_to_children) | |
2169 | Session().commit() |
|
2147 | meta.Session().commit() | |
2170 | return dict( |
|
2148 | return dict( | |
2171 | msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % ( |
|
2149 | msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % ( | |
2172 | apply_to_children, user_group.users_group_name, repo_group.name |
|
2150 | apply_to_children, user_group.users_group_name, repo_group.name | |
@@ -2194,7 +2172,7 b' class ApiController(JSONRPCController):' | |||||
2194 | raise JSONRPCError('gist `%s` does not exist' % (gistid,)) |
|
2172 | raise JSONRPCError('gist `%s` does not exist' % (gistid,)) | |
2195 | return gist.get_api_data() |
|
2173 | return gist.get_api_data() | |
2196 |
|
2174 | |||
2197 |
def get_gists(self, userid= |
|
2175 | def get_gists(self, userid=None): | |
2198 | """ |
|
2176 | """ | |
2199 | Get all gists for given user. If userid is empty returned gists |
|
2177 | Get all gists for given user. If userid is empty returned gists | |
2200 | are for user who called the api |
|
2178 | are for user who called the api | |
@@ -2205,27 +2183,27 b' class ApiController(JSONRPCController):' | |||||
2205 | if not HasPermissionAny('hg.admin')(): |
|
2183 | if not HasPermissionAny('hg.admin')(): | |
2206 | # make sure normal user does not pass someone else userid, |
|
2184 | # make sure normal user does not pass someone else userid, | |
2207 | # he is not allowed to do that |
|
2185 | # he is not allowed to do that | |
2208 |
if not |
|
2186 | if userid is not None and userid != request.authuser.user_id: | |
2209 | raise JSONRPCError( |
|
2187 | raise JSONRPCError( | |
2210 | 'userid is not the same as your user' |
|
2188 | 'userid is not the same as your user' | |
2211 | ) |
|
2189 | ) | |
2212 |
|
2190 | |||
2213 |
if |
|
2191 | if userid is None: | |
2214 | user_id = request.authuser.user_id |
|
2192 | user_id = request.authuser.user_id | |
2215 | else: |
|
2193 | else: | |
2216 | user_id = get_user_or_error(userid).user_id |
|
2194 | user_id = get_user_or_error(userid).user_id | |
2217 |
|
2195 | |||
2218 | return [ |
|
2196 | return [ | |
2219 | gist.get_api_data() |
|
2197 | gist.get_api_data() | |
2220 | for gist in Gist().query() |
|
2198 | for gist in db.Gist().query() | |
2221 | .filter_by(is_expired=False) |
|
2199 | .filter_by(is_expired=False) | |
2222 | .filter(Gist.owner_id == user_id) |
|
2200 | .filter(db.Gist.owner_id == user_id) | |
2223 | .order_by(Gist.created_on.desc()) |
|
2201 | .order_by(db.Gist.created_on.desc()) | |
2224 | ] |
|
2202 | ] | |
2225 |
|
2203 | |||
2226 |
def create_gist(self, files, owner= |
|
2204 | def create_gist(self, files, owner=None, | |
2227 |
gist_type= |
|
2205 | gist_type=db.Gist.GIST_PUBLIC, lifetime=-1, | |
2228 |
description= |
|
2206 | description=''): | |
2229 |
|
2207 | |||
2230 | """ |
|
2208 | """ | |
2231 | Creates new Gist |
|
2209 | Creates new Gist | |
@@ -2262,13 +2240,10 b' class ApiController(JSONRPCController):' | |||||
2262 |
|
2240 | |||
2263 | """ |
|
2241 | """ | |
2264 | try: |
|
2242 | try: | |
2265 |
if |
|
2243 | if owner is None: | |
2266 | owner = request.authuser.user_id |
|
2244 | owner = request.authuser.user_id | |
2267 |
|
2245 | |||
2268 | owner = get_user_or_error(owner) |
|
2246 | owner = get_user_or_error(owner) | |
2269 | description = Optional.extract(description) |
|
|||
2270 | gist_type = Optional.extract(gist_type) |
|
|||
2271 | lifetime = Optional.extract(lifetime) |
|
|||
2272 |
|
2247 | |||
2273 | gist = GistModel().create(description=description, |
|
2248 | gist = GistModel().create(description=description, | |
2274 | owner=owner, |
|
2249 | owner=owner, | |
@@ -2276,7 +2251,7 b' class ApiController(JSONRPCController):' | |||||
2276 | gist_mapping=files, |
|
2251 | gist_mapping=files, | |
2277 | gist_type=gist_type, |
|
2252 | gist_type=gist_type, | |
2278 | lifetime=lifetime) |
|
2253 | lifetime=lifetime) | |
2279 | Session().commit() |
|
2254 | meta.Session().commit() | |
2280 | return dict( |
|
2255 | return dict( | |
2281 | msg='created new gist', |
|
2256 | msg='created new gist', | |
2282 | gist=gist.get_api_data() |
|
2257 | gist=gist.get_api_data() | |
@@ -2285,12 +2260,6 b' class ApiController(JSONRPCController):' | |||||
2285 | log.error(traceback.format_exc()) |
|
2260 | log.error(traceback.format_exc()) | |
2286 | raise JSONRPCError('failed to create gist') |
|
2261 | raise JSONRPCError('failed to create gist') | |
2287 |
|
2262 | |||
2288 | # def update_gist(self, gistid, files, owner=Optional(OAttr('apiuser')), |
|
|||
2289 | # gist_type=Optional(Gist.GIST_PUBLIC), |
|
|||
2290 | # gist_lifetime=Optional(-1), gist_description=Optional('')): |
|
|||
2291 | # gist = get_gist_or_error(gistid) |
|
|||
2292 | # updates = {} |
|
|||
2293 |
|
||||
2294 | # permission check inside |
|
2263 | # permission check inside | |
2295 | def delete_gist(self, gistid): |
|
2264 | def delete_gist(self, gistid): | |
2296 | """ |
|
2265 | """ | |
@@ -2324,7 +2293,7 b' class ApiController(JSONRPCController):' | |||||
2324 |
|
2293 | |||
2325 | try: |
|
2294 | try: | |
2326 | GistModel().delete(gist) |
|
2295 | GistModel().delete(gist) | |
2327 | Session().commit() |
|
2296 | meta.Session().commit() | |
2328 | return dict( |
|
2297 | return dict( | |
2329 | msg='deleted gist ID:%s' % (gist.gist_access_id,), |
|
2298 | msg='deleted gist ID:%s' % (gist.gist_access_id,), | |
2330 | gist=None |
|
2299 | gist=None | |
@@ -2354,7 +2323,7 b' class ApiController(JSONRPCController):' | |||||
2354 | raise JSONRPCError('Repository is empty') |
|
2323 | raise JSONRPCError('Repository is empty') | |
2355 |
|
2324 | |||
2356 | # permission check inside |
|
2325 | # permission check inside | |
2357 |
def get_changeset(self, repoid, raw_id, with_reviews= |
|
2326 | def get_changeset(self, repoid, raw_id, with_reviews=False): | |
2358 | repo = get_repo_or_error(repoid) |
|
2327 | repo = get_repo_or_error(repoid) | |
2359 | if not HasRepoPermissionLevel('read')(repo.repo_name): |
|
2328 | if not HasRepoPermissionLevel('read')(repo.repo_name): | |
2360 | raise JSONRPCError('Access denied to repo %s' % repo.repo_name) |
|
2329 | raise JSONRPCError('Access denied to repo %s' % repo.repo_name) | |
@@ -2364,7 +2333,6 b' class ApiController(JSONRPCController):' | |||||
2364 |
|
2333 | |||
2365 | info = dict(changeset.as_dict()) |
|
2334 | info = dict(changeset.as_dict()) | |
2366 |
|
2335 | |||
2367 | with_reviews = Optional.extract(with_reviews) |
|
|||
2368 | if with_reviews: |
|
2336 | if with_reviews: | |
2369 | reviews = ChangesetStatusModel().get_statuses( |
|
2337 | reviews = ChangesetStatusModel().get_statuses( | |
2370 | repo.repo_name, raw_id) |
|
2338 | repo.repo_name, raw_id) | |
@@ -2377,7 +2345,7 b' class ApiController(JSONRPCController):' | |||||
2377 | """ |
|
2345 | """ | |
2378 | Get given pull request by id |
|
2346 | Get given pull request by id | |
2379 | """ |
|
2347 | """ | |
2380 | pull_request = PullRequest.get(pullrequest_id) |
|
2348 | pull_request = db.PullRequest.get(pullrequest_id) | |
2381 | if pull_request is None: |
|
2349 | if pull_request is None: | |
2382 | raise JSONRPCError('pull request `%s` does not exist' % (pullrequest_id,)) |
|
2350 | raise JSONRPCError('pull request `%s` does not exist' % (pullrequest_id,)) | |
2383 | if not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name): |
|
2351 | if not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name): | |
@@ -2390,7 +2358,7 b' class ApiController(JSONRPCController):' | |||||
2390 | Add comment, close and change status of pull request. |
|
2358 | Add comment, close and change status of pull request. | |
2391 | """ |
|
2359 | """ | |
2392 | apiuser = get_user_or_error(request.authuser.user_id) |
|
2360 | apiuser = get_user_or_error(request.authuser.user_id) | |
2393 | pull_request = PullRequest.get(pull_request_id) |
|
2361 | pull_request = db.PullRequest.get(pull_request_id) | |
2394 | if pull_request is None: |
|
2362 | if pull_request is None: | |
2395 | raise JSONRPCError('pull request `%s` does not exist' % (pull_request_id,)) |
|
2363 | raise JSONRPCError('pull request `%s` does not exist' % (pull_request_id,)) | |
2396 | if (not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name)): |
|
2364 | if (not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name)): | |
@@ -2412,10 +2380,10 b' class ApiController(JSONRPCController):' | |||||
2412 | pull_request=pull_request.pull_request_id, |
|
2380 | pull_request=pull_request.pull_request_id, | |
2413 | f_path=None, |
|
2381 | f_path=None, | |
2414 | line_no=None, |
|
2382 | line_no=None, | |
2415 | status_change=ChangesetStatus.get_status_lbl(status), |
|
2383 | status_change=db.ChangesetStatus.get_status_lbl(status), | |
2416 | closing_pr=close_pr |
|
2384 | closing_pr=close_pr | |
2417 | ) |
|
2385 | ) | |
2418 | action_logger(apiuser, |
|
2386 | userlog.action_logger(apiuser, | |
2419 | 'user_commented_pull_request:%s' % pull_request_id, |
|
2387 | 'user_commented_pull_request:%s' % pull_request_id, | |
2420 | pull_request.org_repo, request.ip_addr) |
|
2388 | pull_request.org_repo, request.ip_addr) | |
2421 | if status: |
|
2389 | if status: | |
@@ -2428,8 +2396,54 b' class ApiController(JSONRPCController):' | |||||
2428 | ) |
|
2396 | ) | |
2429 | if close_pr: |
|
2397 | if close_pr: | |
2430 | PullRequestModel().close_pull_request(pull_request_id) |
|
2398 | PullRequestModel().close_pull_request(pull_request_id) | |
2431 | action_logger(apiuser, |
|
2399 | userlog.action_logger(apiuser, | |
2432 | 'user_closed_pull_request:%s' % pull_request_id, |
|
2400 | 'user_closed_pull_request:%s' % pull_request_id, | |
2433 | pull_request.org_repo, request.ip_addr) |
|
2401 | pull_request.org_repo, request.ip_addr) | |
2434 | Session().commit() |
|
2402 | meta.Session().commit() | |
2435 | return True |
|
2403 | return True | |
|
2404 | ||||
|
2405 | # permission check inside | |||
|
2406 | def edit_reviewers(self, pull_request_id, add=None, remove=None): | |||
|
2407 | """ | |||
|
2408 | Add and/or remove one or more reviewers to a pull request, by username | |||
|
2409 | or user ID. Reviewers are specified either as a single-user string or | |||
|
2410 | as a JSON list of one or more strings. | |||
|
2411 | """ | |||
|
2412 | if add is None and remove is None: | |||
|
2413 | raise JSONRPCError('''Invalid request. Neither 'add' nor 'remove' is specified.''') | |||
|
2414 | ||||
|
2415 | pull_request = db.PullRequest.get(pull_request_id) | |||
|
2416 | if pull_request is None: | |||
|
2417 | raise JSONRPCError('pull request `%s` does not exist' % (pull_request_id,)) | |||
|
2418 | ||||
|
2419 | apiuser = get_user_or_error(request.authuser.user_id) | |||
|
2420 | is_owner = apiuser.user_id == pull_request.owner_id | |||
|
2421 | is_repo_admin = HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name) | |||
|
2422 | if not (apiuser.admin or is_repo_admin or is_owner): | |||
|
2423 | raise JSONRPCError('No permission to edit reviewers of this pull request. User needs to be admin or pull request owner.') | |||
|
2424 | if pull_request.is_closed(): | |||
|
2425 | raise JSONRPCError('Cannot edit reviewers of a closed pull request.') | |||
|
2426 | ||||
|
2427 | if not isinstance(add, list): | |||
|
2428 | add = [add] | |||
|
2429 | if not isinstance(remove, list): | |||
|
2430 | remove = [remove] | |||
|
2431 | ||||
|
2432 | # look up actual user objects from given name or id. Bail out if unknown. | |||
|
2433 | add_objs = set(get_user_or_error(user) for user in add if user is not None) | |||
|
2434 | remove_objs = set(get_user_or_error(user) for user in remove if user is not None) | |||
|
2435 | ||||
|
2436 | new_reviewers = redundant_reviewers = set() | |||
|
2437 | if add_objs: | |||
|
2438 | new_reviewers, redundant_reviewers = PullRequestModel().add_reviewers(apiuser, pull_request, add_objs) | |||
|
2439 | if remove_objs: | |||
|
2440 | PullRequestModel().remove_reviewers(apiuser, pull_request, remove_objs) | |||
|
2441 | ||||
|
2442 | meta.Session().commit() | |||
|
2443 | ||||
|
2444 | return { | |||
|
2445 | 'added': [x.username for x in new_reviewers], | |||
|
2446 | 'already_present': [x.username for x in redundant_reviewers], | |||
|
2447 | # NOTE: no explicit check that removed reviewers were actually present. | |||
|
2448 | 'removed': [x.username for x in remove_objs], | |||
|
2449 | } |
@@ -13,8 +13,8 b'' | |||||
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
14 |
|
14 | |||
15 | """ |
|
15 | """ | |
16 |
kallithea. |
|
16 | kallithea.controllers.base | |
17 | ~~~~~~~~~~~~~~~~~~ |
|
17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
18 |
|
18 | |||
19 | The base Controller API |
|
19 | The base Controller API | |
20 | Provides the BaseController class for subclassing. And usage in different |
|
20 | Provides the BaseController class for subclassing. And usage in different | |
@@ -43,16 +43,15 b' from tg import TGController, config, ren' | |||||
43 | from tg import tmpl_context as c |
|
43 | from tg import tmpl_context as c | |
44 | from tg.i18n import ugettext as _ |
|
44 | from tg.i18n import ugettext as _ | |
45 |
|
45 | |||
46 | from kallithea import BACKENDS, __version__ |
|
46 | import kallithea | |
47 | from kallithea.config.routing import url |
|
47 | from kallithea.lib import auth_modules, ext_json, webutils | |
48 | from kallithea.lib import auth_modules, ext_json |
|
|||
49 | from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware |
|
48 | from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware | |
50 | from kallithea.lib.exceptions import UserCreationError |
|
49 | from kallithea.lib.exceptions import UserCreationError | |
51 | from kallithea.lib.utils import get_repo_slug, is_valid_repo |
|
50 | from kallithea.lib.utils import get_repo_slug, is_valid_repo | |
52 |
from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment |
|
51 | from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment | |
53 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError |
|
52 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError | |
54 |
from kallithea. |
|
53 | from kallithea.lib.webutils import url | |
55 |
from kallithea.model |
|
54 | from kallithea.model import db, meta | |
56 | from kallithea.model.scm import ScmModel |
|
55 | from kallithea.model.scm import ScmModel | |
57 |
|
56 | |||
58 |
|
57 | |||
@@ -65,35 +64,29 b' def render(template_path):' | |||||
65 |
|
64 | |||
66 | def _filter_proxy(ip): |
|
65 | def _filter_proxy(ip): | |
67 | """ |
|
66 | """ | |
68 | HEADERS can have multiple ips inside the left-most being the original |
|
67 | HTTP_X_FORWARDED_FOR headers can have multiple IP addresses, with the | |
69 | client, and each successive proxy that passed the request adding the IP |
|
68 | leftmost being the original client. Each proxy that is forwarding the | |
70 | address where it received the request from. |
|
69 | request will usually add the IP address it sees the request coming from. | |
71 |
|
70 | |||
72 | :param ip: |
|
71 | The client might have provided a fake leftmost value before hitting the | |
|
72 | first proxy, so if we have a proxy that is adding one IP address, we can | |||
|
73 | only trust the rightmost address. | |||
73 | """ |
|
74 | """ | |
74 | if ',' in ip: |
|
75 | if ',' in ip: | |
75 | _ips = ip.split(',') |
|
76 | _ips = ip.split(',') | |
76 |
_first_ip = _ips[ |
|
77 | _first_ip = _ips[-1].strip() | |
77 | log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip) |
|
78 | log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip) | |
78 | return _first_ip |
|
79 | return _first_ip | |
79 | return ip |
|
80 | return ip | |
80 |
|
81 | |||
81 |
|
82 | |||
82 |
def |
|
83 | def get_ip_addr(environ): | |
83 | proxy_key = 'HTTP_X_REAL_IP' |
|
84 | """The web server will set REMOTE_ADDR to the unfakeable IP layer client IP address. | |
84 | proxy_key2 = 'HTTP_X_FORWARDED_FOR' |
|
85 | If using a proxy server, make it possible to use another value, such as | |
85 | def_key = 'REMOTE_ADDR' |
|
86 | the X-Forwarded-For header, by setting `remote_addr_variable = HTTP_X_FORWARDED_FOR`. | |
86 |
|
87 | """ | ||
87 | ip = environ.get(proxy_key) |
|
88 | remote_addr_variable = kallithea.CONFIG.get('remote_addr_variable', 'REMOTE_ADDR') | |
88 | if ip: |
|
89 | return _filter_proxy(environ.get(remote_addr_variable, '0.0.0.0')) | |
89 | return _filter_proxy(ip) |
|
|||
90 |
|
||||
91 | ip = environ.get(proxy_key2) |
|
|||
92 | if ip: |
|
|||
93 | return _filter_proxy(ip) |
|
|||
94 |
|
||||
95 | ip = environ.get(def_key, '0.0.0.0') |
|
|||
96 | return _filter_proxy(ip) |
|
|||
97 |
|
90 | |||
98 |
|
91 | |||
99 | def get_path_info(environ): |
|
92 | def get_path_info(environ): | |
@@ -223,7 +216,7 b' class BaseVCSController(object):' | |||||
223 | Returns (None, wsgi_app) to send the wsgi_app response to the client. |
|
216 | Returns (None, wsgi_app) to send the wsgi_app response to the client. | |
224 | """ |
|
217 | """ | |
225 | # Use anonymous access if allowed for action on repo. |
|
218 | # Use anonymous access if allowed for action on repo. | |
226 | default_user = User.get_default_user() |
|
219 | default_user = db.User.get_default_user() | |
227 | default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) |
|
220 | default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) | |
228 | if default_authuser is None: |
|
221 | if default_authuser is None: | |
229 | log.debug('No anonymous access at all') # move on to proper user auth |
|
222 | log.debug('No anonymous access at all') # move on to proper user auth | |
@@ -260,7 +253,7 b' class BaseVCSController(object):' | |||||
260 | # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME |
|
253 | # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME | |
261 | #============================================================== |
|
254 | #============================================================== | |
262 | try: |
|
255 | try: | |
263 | user = User.get_by_username_or_email(username) |
|
256 | user = db.User.get_by_username_or_email(username) | |
264 | except Exception: |
|
257 | except Exception: | |
265 | log.error(traceback.format_exc()) |
|
258 | log.error(traceback.format_exc()) | |
266 | return None, webob.exc.HTTPInternalServerError() |
|
259 | return None, webob.exc.HTTPInternalServerError() | |
@@ -301,9 +294,6 b' class BaseVCSController(object):' | |||||
301 |
|
294 | |||
302 | return True |
|
295 | return True | |
303 |
|
296 | |||
304 | def _get_ip_addr(self, environ): |
|
|||
305 | return _get_ip_addr(environ) |
|
|||
306 |
|
||||
307 | def __call__(self, environ, start_response): |
|
297 | def __call__(self, environ, start_response): | |
308 | try: |
|
298 | try: | |
309 | # try parsing a request for this VCS - if it fails, call the wrapped app |
|
299 | # try parsing a request for this VCS - if it fails, call the wrapped app | |
@@ -325,7 +315,7 b' class BaseVCSController(object):' | |||||
325 | #====================================================================== |
|
315 | #====================================================================== | |
326 | # CHECK PERMISSIONS |
|
316 | # CHECK PERMISSIONS | |
327 | #====================================================================== |
|
317 | #====================================================================== | |
328 |
ip_addr = |
|
318 | ip_addr = get_ip_addr(environ) | |
329 | user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr) |
|
319 | user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr) | |
330 | if response_app is not None: |
|
320 | if response_app is not None: | |
331 | return response_app(environ, start_response) |
|
321 | return response_app(environ, start_response) | |
@@ -362,30 +352,29 b' class BaseController(TGController):' | |||||
362 | # guaranteed to be side effect free. In practice, the only situation |
|
352 | # guaranteed to be side effect free. In practice, the only situation | |
363 | # where we allow side effects without ambient authority is when the |
|
353 | # where we allow side effects without ambient authority is when the | |
364 | # authority comes from an API key; and that is handled above. |
|
354 | # authority comes from an API key; and that is handled above. | |
365 | from kallithea.lib import helpers as h |
|
355 | token = request.POST.get(webutils.session_csrf_secret_name) | |
366 |
token |
|
356 | if not token or token != webutils.session_csrf_secret_token(): | |
367 | if not token or token != h.session_csrf_secret_token(): |
|
|||
368 | log.error('CSRF check failed') |
|
357 | log.error('CSRF check failed') | |
369 | raise webob.exc.HTTPForbidden() |
|
358 | raise webob.exc.HTTPForbidden() | |
370 |
|
359 | |||
371 | c.kallithea_version = __version__ |
|
360 | c.kallithea_version = kallithea.__version__ | |
372 |
|
|
361 | settings = db.Setting.get_app_settings() | |
373 |
|
362 | |||
374 | # Visual options |
|
363 | # Visual options | |
375 | c.visual = AttributeDict({}) |
|
364 | c.visual = AttributeDict({}) | |
376 |
|
365 | |||
377 | ## DB stored |
|
366 | ## DB stored | |
378 |
c.visual.show_public_icon = s |
|
367 | c.visual.show_public_icon = asbool(settings.get('show_public_icon')) | |
379 |
c.visual.show_private_icon = s |
|
368 | c.visual.show_private_icon = asbool(settings.get('show_private_icon')) | |
380 |
c.visual.stylify_metalabels = s |
|
369 | c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels')) | |
381 |
c.visual.page_size = safe_int( |
|
370 | c.visual.page_size = safe_int(settings.get('dashboard_items', 100)) | |
382 |
c.visual.admin_grid_items = safe_int( |
|
371 | c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100)) | |
383 |
c.visual.repository_fields = s |
|
372 | c.visual.repository_fields = asbool(settings.get('repository_fields')) | |
384 |
c.visual.show_version = s |
|
373 | c.visual.show_version = asbool(settings.get('show_version')) | |
385 |
c.visual.use_gravatar = s |
|
374 | c.visual.use_gravatar = asbool(settings.get('use_gravatar')) | |
386 |
c.visual.gravatar_url = |
|
375 | c.visual.gravatar_url = settings.get('gravatar_url') | |
387 |
|
376 | |||
388 |
c.ga_code = |
|
377 | c.ga_code = settings.get('ga_code') | |
389 | # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code |
|
378 | # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code | |
390 | if c.ga_code and '<' not in c.ga_code: |
|
379 | if c.ga_code and '<' not in c.ga_code: | |
391 | c.ga_code = '''<script type="text/javascript"> |
|
380 | c.ga_code = '''<script type="text/javascript"> | |
@@ -399,25 +388,25 b' class BaseController(TGController):' | |||||
399 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); |
|
388 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); | |
400 | })(); |
|
389 | })(); | |
401 | </script>''' % c.ga_code |
|
390 | </script>''' % c.ga_code | |
402 |
c.site_name = |
|
391 | c.site_name = settings.get('title') | |
403 |
c.clone_uri_tmpl = |
|
392 | c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI | |
404 |
c.clone_ssh_tmpl = |
|
393 | c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH | |
405 |
|
394 | |||
406 | ## INI stored |
|
395 | ## INI stored | |
407 |
c.visual.allow_repo_location_change = s |
|
396 | c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True)) | |
408 |
c.visual.allow_custom_hooks_settings = s |
|
397 | c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True)) | |
409 |
c.ssh_enabled = s |
|
398 | c.ssh_enabled = asbool(config.get('ssh_enabled', False)) | |
410 |
|
399 | |||
411 | c.instance_id = config.get('instance_id') |
|
400 | c.instance_id = config.get('instance_id') | |
412 | c.issues_url = config.get('bugtracker', url('issues_url')) |
|
401 | c.issues_url = config.get('bugtracker', url('issues_url')) | |
413 | # END CONFIG VARS |
|
402 | # END CONFIG VARS | |
414 |
|
403 | |||
415 | c.repo_name = get_repo_slug(request) # can be empty |
|
404 | c.repo_name = get_repo_slug(request) # can be empty | |
416 | c.backends = list(BACKENDS) |
|
405 | c.backends = list(kallithea.BACKENDS) | |
417 |
|
406 | |||
418 | self.cut_off_limit = safe_int(config.get('cut_off_limit')) |
|
407 | self.cut_off_limit = safe_int(config.get('cut_off_limit')) | |
419 |
|
408 | |||
420 | c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() |
|
409 | c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() | |
421 |
|
410 | |||
422 | self.scm_model = ScmModel() |
|
411 | self.scm_model = ScmModel() | |
423 |
|
412 | |||
@@ -445,16 +434,15 b' class BaseController(TGController):' | |||||
445 | try: |
|
434 | try: | |
446 | user_info = auth_modules.authenticate('', '', request.environ) |
|
435 | user_info = auth_modules.authenticate('', '', request.environ) | |
447 | except UserCreationError as e: |
|
436 | except UserCreationError as e: | |
448 | from kallithea.lib import helpers as h |
|
437 | webutils.flash(e, 'error', logf=log.error) | |
449 | h.flash(e, 'error', logf=log.error) |
|
|||
450 | else: |
|
438 | else: | |
451 | if user_info is not None: |
|
439 | if user_info is not None: | |
452 | username = user_info['username'] |
|
440 | username = user_info['username'] | |
453 | user = User.get_by_username(username, case_insensitive=True) |
|
441 | user = db.User.get_by_username(username, case_insensitive=True) | |
454 | return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr) |
|
442 | return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr) | |
455 |
|
443 | |||
456 | # User is default user (if active) or anonymous |
|
444 | # User is default user (if active) or anonymous | |
457 | default_user = User.get_default_user() |
|
445 | default_user = db.User.get_default_user() | |
458 | authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) |
|
446 | authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr) | |
459 | if authuser is None: # fall back to anonymous |
|
447 | if authuser is None: # fall back to anonymous | |
460 | authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make? |
|
448 | authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make? | |
@@ -475,12 +463,11 b' class BaseController(TGController):' | |||||
475 | raise webob.exc.HTTPMethodNotAllowed() |
|
463 | raise webob.exc.HTTPMethodNotAllowed() | |
476 |
|
464 | |||
477 | # Make sure CSRF token never appears in the URL. If so, invalidate it. |
|
465 | # Make sure CSRF token never appears in the URL. If so, invalidate it. | |
478 | from kallithea.lib import helpers as h |
|
466 | if webutils.session_csrf_secret_name in request.GET: | |
479 | if h.session_csrf_secret_name in request.GET: |
|
|||
480 | log.error('CSRF key leak detected') |
|
467 | log.error('CSRF key leak detected') | |
481 |
session.pop( |
|
468 | session.pop(webutils.session_csrf_secret_name, None) | |
482 | session.save() |
|
469 | session.save() | |
483 |
|
|
470 | webutils.flash(_('CSRF token leak has been detected - all form tokens have been expired'), | |
484 | category='error') |
|
471 | category='error') | |
485 |
|
472 | |||
486 | # WebOb already ignores request payload parameters for anything other |
|
473 | # WebOb already ignores request payload parameters for anything other | |
@@ -492,7 +479,7 b' class BaseController(TGController):' | |||||
492 |
|
479 | |||
493 | def __call__(self, environ, context): |
|
480 | def __call__(self, environ, context): | |
494 | try: |
|
481 | try: | |
495 |
ip_addr = |
|
482 | ip_addr = get_ip_addr(environ) | |
496 | self._basic_security_checks() |
|
483 | self._basic_security_checks() | |
497 |
|
484 | |||
498 | api_key = request.GET.get('api_key') |
|
485 | api_key = request.GET.get('api_key') | |
@@ -513,7 +500,7 b' class BaseController(TGController):' | |||||
513 | needs_csrf_check = request.method not in ['GET', 'HEAD'] |
|
500 | needs_csrf_check = request.method not in ['GET', 'HEAD'] | |
514 |
|
501 | |||
515 | else: |
|
502 | else: | |
516 | dbuser = User.get_by_api_key(api_key) |
|
503 | dbuser = db.User.get_by_api_key(api_key) | |
517 | if dbuser is None: |
|
504 | if dbuser is None: | |
518 | log.info('No db user found for authentication with API key ****%s from %s', |
|
505 | log.info('No db user found for authentication with API key ****%s from %s', | |
519 | api_key[-4:], ip_addr) |
|
506 | api_key[-4:], ip_addr) | |
@@ -553,7 +540,7 b' class BaseRepoController(BaseController)' | |||||
553 | def _before(self, *args, **kwargs): |
|
540 | def _before(self, *args, **kwargs): | |
554 | super(BaseRepoController, self)._before(*args, **kwargs) |
|
541 | super(BaseRepoController, self)._before(*args, **kwargs) | |
555 | if c.repo_name: # extracted from request by base-base BaseController._before |
|
542 | if c.repo_name: # extracted from request by base-base BaseController._before | |
556 | _dbr = Repository.get_by_repo_name(c.repo_name) |
|
543 | _dbr = db.Repository.get_by_repo_name(c.repo_name) | |
557 | if not _dbr: |
|
544 | if not _dbr: | |
558 | return |
|
545 | return | |
559 |
|
546 | |||
@@ -565,7 +552,7 b' class BaseRepoController(BaseController)' | |||||
565 | if route in ['delete_repo']: |
|
552 | if route in ['delete_repo']: | |
566 | return |
|
553 | return | |
567 |
|
554 | |||
568 | if _dbr.repo_state in [Repository.STATE_PENDING]: |
|
555 | if _dbr.repo_state in [db.Repository.STATE_PENDING]: | |
569 | if route in ['repo_creating_home']: |
|
556 | if route in ['repo_creating_home']: | |
570 | return |
|
557 | return | |
571 | check_url = url('repo_creating_home', repo_name=c.repo_name) |
|
558 | check_url = url('repo_creating_home', repo_name=c.repo_name) | |
@@ -576,8 +563,7 b' class BaseRepoController(BaseController)' | |||||
576 | if c.db_repo_scm_instance is None: |
|
563 | if c.db_repo_scm_instance is None: | |
577 | log.error('%s this repository is present in database but it ' |
|
564 | log.error('%s this repository is present in database but it ' | |
578 | 'cannot be created as an scm instance', c.repo_name) |
|
565 | 'cannot be created as an scm instance', c.repo_name) | |
579 | from kallithea.lib import helpers as h |
|
566 | webutils.flash(_('Repository not found in the filesystem'), | |
580 | h.flash(_('Repository not found in the filesystem'), |
|
|||
581 | category='error') |
|
567 | category='error') | |
582 | raise webob.exc.HTTPNotFound() |
|
568 | raise webob.exc.HTTPNotFound() | |
583 |
|
569 | |||
@@ -593,22 +579,21 b' class BaseRepoController(BaseController)' | |||||
593 | """ |
|
579 | """ | |
594 | Safe way to get changeset. If error occurs show error. |
|
580 | Safe way to get changeset. If error occurs show error. | |
595 | """ |
|
581 | """ | |
596 | from kallithea.lib import helpers as h |
|
|||
597 | try: |
|
582 | try: | |
598 | return repo.scm_instance.get_ref_revision(ref_type, ref_name) |
|
583 | return repo.scm_instance.get_ref_revision(ref_type, ref_name) | |
599 | except EmptyRepositoryError as e: |
|
584 | except EmptyRepositoryError as e: | |
600 | if returnempty: |
|
585 | if returnempty: | |
601 | return repo.scm_instance.EMPTY_CHANGESET |
|
586 | return repo.scm_instance.EMPTY_CHANGESET | |
602 |
|
|
587 | webutils.flash(_('There are no changesets yet'), category='error') | |
603 | raise webob.exc.HTTPNotFound() |
|
588 | raise webob.exc.HTTPNotFound() | |
604 | except ChangesetDoesNotExistError as e: |
|
589 | except ChangesetDoesNotExistError as e: | |
605 |
|
|
590 | webutils.flash(_('Changeset for %s %s not found in %s') % | |
606 | (ref_type, ref_name, repo.repo_name), |
|
591 | (ref_type, ref_name, repo.repo_name), | |
607 | category='error') |
|
592 | category='error') | |
608 | raise webob.exc.HTTPNotFound() |
|
593 | raise webob.exc.HTTPNotFound() | |
609 | except RepositoryError as e: |
|
594 | except RepositoryError as e: | |
610 | log.error(traceback.format_exc()) |
|
595 | log.error(traceback.format_exc()) | |
611 |
|
|
596 | webutils.flash(e, category='error') | |
612 | raise webob.exc.HTTPBadRequest() |
|
597 | raise webob.exc.HTTPBadRequest() | |
613 |
|
598 | |||
614 |
|
599 | |||
@@ -643,7 +628,6 b' def IfSshEnabled(func, *args, **kwargs):' | |||||
643 | If SSH access is disabled in the configuration file, HTTPNotFound is raised. |
|
628 | If SSH access is disabled in the configuration file, HTTPNotFound is raised. | |
644 | """ |
|
629 | """ | |
645 | if not c.ssh_enabled: |
|
630 | if not c.ssh_enabled: | |
646 | from kallithea.lib import helpers as h |
|
631 | webutils.flash(_("SSH access is disabled."), category='warning') | |
647 | h.flash(_("SSH access is disabled."), category='warning') |
|
|||
648 | raise webob.exc.HTTPNotFound() |
|
632 | raise webob.exc.HTTPNotFound() | |
649 | return func(*args, **kwargs) |
|
633 | return func(*args, **kwargs) |
@@ -33,20 +33,20 b' from tg import tmpl_context as c' | |||||
33 | from tg.i18n import ugettext as _ |
|
33 | from tg.i18n import ugettext as _ | |
34 | from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound |
|
34 | from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound | |
35 |
|
35 | |||
36 | import kallithea.lib.helpers as h |
|
36 | from kallithea.controllers import base | |
37 |
from kallithea. |
|
37 | from kallithea.lib import webutils | |
38 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
38 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
39 | from kallithea.lib.base import BaseRepoController, render |
|
|||
40 | from kallithea.lib.graphmod import graph_data |
|
39 | from kallithea.lib.graphmod import graph_data | |
41 | from kallithea.lib.page import Page |
|
40 | from kallithea.lib.page import Page | |
42 | from kallithea.lib.utils2 import safe_int |
|
41 | from kallithea.lib.utils2 import safe_int | |
43 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, NodeDoesNotExistError, RepositoryError |
|
42 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, NodeDoesNotExistError, RepositoryError | |
|
43 | from kallithea.lib.webutils import url | |||
44 |
|
44 | |||
45 |
|
45 | |||
46 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
47 |
|
47 | |||
48 |
|
48 | |||
49 | class ChangelogController(BaseRepoController): |
|
49 | class ChangelogController(base.BaseRepoController): | |
50 |
|
50 | |||
51 | def _before(self, *args, **kwargs): |
|
51 | def _before(self, *args, **kwargs): | |
52 | super(ChangelogController, self)._before(*args, **kwargs) |
|
52 | super(ChangelogController, self)._before(*args, **kwargs) | |
@@ -64,10 +64,10 b' class ChangelogController(BaseRepoContro' | |||||
64 | try: |
|
64 | try: | |
65 | return c.db_repo_scm_instance.get_changeset(rev) |
|
65 | return c.db_repo_scm_instance.get_changeset(rev) | |
66 | except EmptyRepositoryError as e: |
|
66 | except EmptyRepositoryError as e: | |
67 |
|
|
67 | webutils.flash(_('There are no changesets yet'), category='error') | |
68 | except RepositoryError as e: |
|
68 | except RepositoryError as e: | |
69 | log.error(traceback.format_exc()) |
|
69 | log.error(traceback.format_exc()) | |
70 |
|
|
70 | webutils.flash(e, category='error') | |
71 | raise HTTPBadRequest() |
|
71 | raise HTTPBadRequest() | |
72 |
|
72 | |||
73 | @LoginRequired(allow_default_user=True) |
|
73 | @LoginRequired(allow_default_user=True) | |
@@ -111,8 +111,8 b' class ChangelogController(BaseRepoContro' | |||||
111 | cs = self.__get_cs(revision, repo_name) |
|
111 | cs = self.__get_cs(revision, repo_name) | |
112 | collection = cs.get_file_history(f_path) |
|
112 | collection = cs.get_file_history(f_path) | |
113 | except RepositoryError as e: |
|
113 | except RepositoryError as e: | |
114 |
|
|
114 | webutils.flash(e, category='warning') | |
115 |
raise HTTPFound(location= |
|
115 | raise HTTPFound(location=webutils.url('changelog_home', repo_name=repo_name)) | |
116 | else: |
|
116 | else: | |
117 | collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision, |
|
117 | collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision, | |
118 | branch_name=branch_name, reverse=True) |
|
118 | branch_name=branch_name, reverse=True) | |
@@ -125,11 +125,11 b' class ChangelogController(BaseRepoContro' | |||||
125 | c.cs_comments = c.db_repo.get_comments(page_revisions) |
|
125 | c.cs_comments = c.db_repo.get_comments(page_revisions) | |
126 | c.cs_statuses = c.db_repo.statuses(page_revisions) |
|
126 | c.cs_statuses = c.db_repo.statuses(page_revisions) | |
127 | except EmptyRepositoryError as e: |
|
127 | except EmptyRepositoryError as e: | |
128 |
|
|
128 | webutils.flash(e, category='warning') | |
129 | raise HTTPFound(location=url('summary_home', repo_name=c.repo_name)) |
|
129 | raise HTTPFound(location=url('summary_home', repo_name=c.repo_name)) | |
130 | except (RepositoryError, ChangesetDoesNotExistError, Exception) as e: |
|
130 | except (RepositoryError, ChangesetDoesNotExistError, Exception) as e: | |
131 | log.error(traceback.format_exc()) |
|
131 | log.error(traceback.format_exc()) | |
132 |
|
|
132 | webutils.flash(e, category='error') | |
133 | raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name)) |
|
133 | raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name)) | |
134 |
|
134 | |||
135 | c.branch_name = branch_name |
|
135 | c.branch_name = branch_name | |
@@ -146,12 +146,12 b' class ChangelogController(BaseRepoContro' | |||||
146 |
|
146 | |||
147 | c.revision = revision # requested revision ref |
|
147 | c.revision = revision # requested revision ref | |
148 | c.first_revision = c.cs_pagination[0] # pagination is never empty here! |
|
148 | c.first_revision = c.cs_pagination[0] # pagination is never empty here! | |
149 | return render('changelog/changelog.html') |
|
149 | return base.render('changelog/changelog.html') | |
150 |
|
150 | |||
151 | @LoginRequired(allow_default_user=True) |
|
151 | @LoginRequired(allow_default_user=True) | |
152 | @HasRepoPermissionLevelDecorator('read') |
|
152 | @HasRepoPermissionLevelDecorator('read') | |
153 | def changelog_details(self, cs): |
|
153 | def changelog_details(self, cs): | |
154 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
154 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
155 | c.cs = c.db_repo_scm_instance.get_changeset(cs) |
|
155 | c.cs = c.db_repo_scm_instance.get_changeset(cs) | |
156 | return render('changelog/changelog_details.html') |
|
156 | return base.render('changelog/changelog_details.html') | |
157 | raise HTTPNotFound() |
|
157 | raise HTTPNotFound() |
@@ -28,7 +28,7 b' Original author and date, and relevant c' | |||||
28 | import binascii |
|
28 | import binascii | |
29 | import logging |
|
29 | import logging | |
30 | import traceback |
|
30 | import traceback | |
31 |
from collections import OrderedDict |
|
31 | from collections import OrderedDict | |
32 |
|
32 | |||
33 | from tg import request, response |
|
33 | from tg import request, response | |
34 | from tg import tmpl_context as c |
|
34 | from tg import tmpl_context as c | |
@@ -36,136 +36,22 b' from tg.i18n import ugettext as _' | |||||
36 | from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound |
|
36 | from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound | |
37 |
|
37 | |||
38 | import kallithea.lib.helpers as h |
|
38 | import kallithea.lib.helpers as h | |
39 |
from kallithea. |
|
39 | from kallithea.controllers import base | |
|
40 | from kallithea.lib import auth, diffs, webutils | |||
40 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
41 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
41 | from kallithea.lib.base import BaseRepoController, jsonify, render |
|
|||
42 | from kallithea.lib.graphmod import graph_data |
|
42 | from kallithea.lib.graphmod import graph_data | |
43 | from kallithea.lib.utils import action_logger |
|
|||
44 | from kallithea.lib.utils2 import ascii_str, safe_str |
|
43 | from kallithea.lib.utils2 import ascii_str, safe_str | |
45 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
44 | from kallithea.lib.vcs.backends.base import EmptyChangeset | |
46 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError |
|
45 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError | |
|
46 | from kallithea.model import db, meta, userlog | |||
47 | from kallithea.model.changeset_status import ChangesetStatusModel |
|
47 | from kallithea.model.changeset_status import ChangesetStatusModel | |
48 | from kallithea.model.comment import ChangesetCommentsModel |
|
48 | from kallithea.model.comment import ChangesetCommentsModel | |
49 | from kallithea.model.db import ChangesetComment, ChangesetStatus |
|
|||
50 | from kallithea.model.meta import Session |
|
|||
51 | from kallithea.model.pull_request import PullRequestModel |
|
49 | from kallithea.model.pull_request import PullRequestModel | |
52 |
|
50 | |||
53 |
|
51 | |||
54 | log = logging.getLogger(__name__) |
|
52 | log = logging.getLogger(__name__) | |
55 |
|
53 | |||
56 |
|
54 | |||
57 | def _update_with_GET(params, GET): |
|
|||
58 | for k in ['diff1', 'diff2', 'diff']: |
|
|||
59 | params[k] += GET.getall(k) |
|
|||
60 |
|
||||
61 |
|
||||
62 | def anchor_url(revision, path, GET): |
|
|||
63 | fid = h.FID(revision, path) |
|
|||
64 | return h.url.current(anchor=fid, **dict(GET)) |
|
|||
65 |
|
||||
66 |
|
||||
67 | def get_ignore_ws(fid, GET): |
|
|||
68 | ig_ws_global = GET.get('ignorews') |
|
|||
69 | ig_ws = [k for k in GET.getall(fid) if k.startswith('WS')] |
|
|||
70 | if ig_ws: |
|
|||
71 | try: |
|
|||
72 | return int(ig_ws[0].split(':')[-1]) |
|
|||
73 | except ValueError: |
|
|||
74 | raise HTTPBadRequest() |
|
|||
75 | return ig_ws_global |
|
|||
76 |
|
||||
77 |
|
||||
78 | def _ignorews_url(GET, fileid=None): |
|
|||
79 | fileid = str(fileid) if fileid else None |
|
|||
80 | params = defaultdict(list) |
|
|||
81 | _update_with_GET(params, GET) |
|
|||
82 | lbl = _('Show whitespace') |
|
|||
83 | ig_ws = get_ignore_ws(fileid, GET) |
|
|||
84 | ln_ctx = get_line_ctx(fileid, GET) |
|
|||
85 | # global option |
|
|||
86 | if fileid is None: |
|
|||
87 | if ig_ws is None: |
|
|||
88 | params['ignorews'] += [1] |
|
|||
89 | lbl = _('Ignore whitespace') |
|
|||
90 | ctx_key = 'context' |
|
|||
91 | ctx_val = ln_ctx |
|
|||
92 | # per file options |
|
|||
93 | else: |
|
|||
94 | if ig_ws is None: |
|
|||
95 | params[fileid] += ['WS:1'] |
|
|||
96 | lbl = _('Ignore whitespace') |
|
|||
97 |
|
||||
98 | ctx_key = fileid |
|
|||
99 | ctx_val = 'C:%s' % ln_ctx |
|
|||
100 | # if we have passed in ln_ctx pass it along to our params |
|
|||
101 | if ln_ctx: |
|
|||
102 | params[ctx_key] += [ctx_val] |
|
|||
103 |
|
||||
104 | params['anchor'] = fileid |
|
|||
105 | icon = h.literal('<i class="icon-strike"></i>') |
|
|||
106 | return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'}) |
|
|||
107 |
|
||||
108 |
|
||||
109 | def get_line_ctx(fid, GET): |
|
|||
110 | ln_ctx_global = GET.get('context') |
|
|||
111 | if fid: |
|
|||
112 | ln_ctx = [k for k in GET.getall(fid) if k.startswith('C')] |
|
|||
113 | else: |
|
|||
114 | _ln_ctx = [k for k in GET if k.startswith('C')] |
|
|||
115 | ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global |
|
|||
116 | if ln_ctx: |
|
|||
117 | ln_ctx = [ln_ctx] |
|
|||
118 |
|
||||
119 | if ln_ctx: |
|
|||
120 | retval = ln_ctx[0].split(':')[-1] |
|
|||
121 | else: |
|
|||
122 | retval = ln_ctx_global |
|
|||
123 |
|
||||
124 | try: |
|
|||
125 | return int(retval) |
|
|||
126 | except Exception: |
|
|||
127 | return 3 |
|
|||
128 |
|
||||
129 |
|
||||
130 | def _context_url(GET, fileid=None): |
|
|||
131 | """ |
|
|||
132 | Generates url for context lines |
|
|||
133 |
|
||||
134 | :param fileid: |
|
|||
135 | """ |
|
|||
136 |
|
||||
137 | fileid = str(fileid) if fileid else None |
|
|||
138 | ig_ws = get_ignore_ws(fileid, GET) |
|
|||
139 | ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2 |
|
|||
140 |
|
||||
141 | params = defaultdict(list) |
|
|||
142 | _update_with_GET(params, GET) |
|
|||
143 |
|
||||
144 | # global option |
|
|||
145 | if fileid is None: |
|
|||
146 | if ln_ctx > 0: |
|
|||
147 | params['context'] += [ln_ctx] |
|
|||
148 |
|
||||
149 | if ig_ws: |
|
|||
150 | ig_ws_key = 'ignorews' |
|
|||
151 | ig_ws_val = 1 |
|
|||
152 |
|
||||
153 | # per file option |
|
|||
154 | else: |
|
|||
155 | params[fileid] += ['C:%s' % ln_ctx] |
|
|||
156 | ig_ws_key = fileid |
|
|||
157 | ig_ws_val = 'WS:%s' % 1 |
|
|||
158 |
|
||||
159 | if ig_ws: |
|
|||
160 | params[ig_ws_key] += [ig_ws_val] |
|
|||
161 |
|
||||
162 | lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx} |
|
|||
163 |
|
||||
164 | params['anchor'] = fileid |
|
|||
165 | icon = h.literal('<i class="icon-sort"></i>') |
|
|||
166 | return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'}) |
|
|||
167 |
|
||||
168 |
|
||||
169 | def create_cs_pr_comment(repo_name, revision=None, pull_request=None, allowed_to_change_status=True): |
|
55 | def create_cs_pr_comment(repo_name, revision=None, pull_request=None, allowed_to_change_status=True): | |
170 | """ |
|
56 | """ | |
171 | Add a comment to the specified changeset or pull request, using POST values |
|
57 | Add a comment to the specified changeset or pull request, using POST values | |
@@ -199,21 +85,21 b' def create_cs_pr_comment(repo_name, revi' | |||||
199 |
|
85 | |||
200 | if not allowed_to_change_status: |
|
86 | if not allowed_to_change_status: | |
201 | if status or close_pr: |
|
87 | if status or close_pr: | |
202 |
|
|
88 | webutils.flash(_('No permission to change status'), 'error') | |
203 | raise HTTPForbidden() |
|
89 | raise HTTPForbidden() | |
204 |
|
90 | |||
205 | if pull_request and delete == "delete": |
|
91 | if pull_request and delete == "delete": | |
206 | if (pull_request.owner_id == request.authuser.user_id or |
|
92 | if (pull_request.owner_id == request.authuser.user_id or | |
207 | h.HasPermissionAny('hg.admin')() or |
|
93 | auth.HasPermissionAny('hg.admin')() or | |
208 | h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or |
|
94 | auth.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or | |
209 | h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name) |
|
95 | auth.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name) | |
210 | ) and not pull_request.is_closed(): |
|
96 | ) and not pull_request.is_closed(): | |
211 | PullRequestModel().delete(pull_request) |
|
97 | PullRequestModel().delete(pull_request) | |
212 | Session().commit() |
|
98 | meta.Session().commit() | |
213 |
|
|
99 | webutils.flash(_('Successfully deleted pull request %s') % pull_request_id, | |
214 | category='success') |
|
100 | category='success') | |
215 | return { |
|
101 | return { | |
216 |
'location': |
|
102 | 'location': webutils.url('my_pullrequests'), # or repo pr list? | |
217 | } |
|
103 | } | |
218 | raise HTTPForbidden() |
|
104 | raise HTTPForbidden() | |
219 |
|
105 | |||
@@ -227,7 +113,7 b' def create_cs_pr_comment(repo_name, revi' | |||||
227 | pull_request=pull_request_id, |
|
113 | pull_request=pull_request_id, | |
228 | f_path=f_path or None, |
|
114 | f_path=f_path or None, | |
229 | line_no=line_no or None, |
|
115 | line_no=line_no or None, | |
230 | status_change=ChangesetStatus.get_status_lbl(status) if status else None, |
|
116 | status_change=db.ChangesetStatus.get_status_lbl(status) if status else None, | |
231 | closing_pr=close_pr, |
|
117 | closing_pr=close_pr, | |
232 | ) |
|
118 | ) | |
233 |
|
119 | |||
@@ -245,30 +131,30 b' def create_cs_pr_comment(repo_name, revi' | |||||
245 | action = 'user_commented_pull_request:%s' % pull_request_id |
|
131 | action = 'user_commented_pull_request:%s' % pull_request_id | |
246 | else: |
|
132 | else: | |
247 | action = 'user_commented_revision:%s' % revision |
|
133 | action = 'user_commented_revision:%s' % revision | |
248 | action_logger(request.authuser, action, c.db_repo, request.ip_addr) |
|
134 | userlog.action_logger(request.authuser, action, c.db_repo, request.ip_addr) | |
249 |
|
135 | |||
250 | if pull_request and close_pr: |
|
136 | if pull_request and close_pr: | |
251 | PullRequestModel().close_pull_request(pull_request_id) |
|
137 | PullRequestModel().close_pull_request(pull_request_id) | |
252 | action_logger(request.authuser, |
|
138 | userlog.action_logger(request.authuser, | |
253 | 'user_closed_pull_request:%s' % pull_request_id, |
|
139 | 'user_closed_pull_request:%s' % pull_request_id, | |
254 | c.db_repo, request.ip_addr) |
|
140 | c.db_repo, request.ip_addr) | |
255 |
|
141 | |||
256 | Session().commit() |
|
142 | meta.Session().commit() | |
257 |
|
143 | |||
258 | data = { |
|
144 | data = { | |
259 |
'target_id': |
|
145 | 'target_id': webutils.safeid(request.POST.get('f_path')), | |
260 | } |
|
146 | } | |
261 | if comment is not None: |
|
147 | if comment is not None: | |
262 | c.comment = comment |
|
148 | c.comment = comment | |
263 | data.update(comment.get_dict()) |
|
149 | data.update(comment.get_dict()) | |
264 | data.update({'rendered_text': |
|
150 | data.update({'rendered_text': | |
265 | render('changeset/changeset_comment_block.html')}) |
|
151 | base.render('changeset/changeset_comment_block.html')}) | |
266 |
|
152 | |||
267 | return data |
|
153 | return data | |
268 |
|
154 | |||
269 | def delete_cs_pr_comment(repo_name, comment_id): |
|
155 | def delete_cs_pr_comment(repo_name, comment_id): | |
270 | """Delete a comment from a changeset or pull request""" |
|
156 | """Delete a comment from a changeset or pull request""" | |
271 | co = ChangesetComment.get_or_404(comment_id) |
|
157 | co = db.ChangesetComment.get_or_404(comment_id) | |
272 | if co.repo.repo_name != repo_name: |
|
158 | if co.repo.repo_name != repo_name: | |
273 | raise HTTPNotFound() |
|
159 | raise HTTPNotFound() | |
274 | if co.pull_request and co.pull_request.is_closed(): |
|
160 | if co.pull_request and co.pull_request.is_closed(): | |
@@ -276,15 +162,15 b' def delete_cs_pr_comment(repo_name, comm' | |||||
276 | raise HTTPForbidden() |
|
162 | raise HTTPForbidden() | |
277 |
|
163 | |||
278 | owner = co.author_id == request.authuser.user_id |
|
164 | owner = co.author_id == request.authuser.user_id | |
279 | repo_admin = h.HasRepoPermissionLevel('admin')(repo_name) |
|
165 | repo_admin = auth.HasRepoPermissionLevel('admin')(repo_name) | |
280 | if h.HasPermissionAny('hg.admin')() or repo_admin or owner: |
|
166 | if auth.HasPermissionAny('hg.admin')() or repo_admin or owner: | |
281 | ChangesetCommentsModel().delete(comment=co) |
|
167 | ChangesetCommentsModel().delete(comment=co) | |
282 | Session().commit() |
|
168 | meta.Session().commit() | |
283 | return True |
|
169 | return True | |
284 | else: |
|
170 | else: | |
285 | raise HTTPForbidden() |
|
171 | raise HTTPForbidden() | |
286 |
|
172 | |||
287 | class ChangesetController(BaseRepoController): |
|
173 | class ChangesetController(base.BaseRepoController): | |
288 |
|
174 | |||
289 | def _before(self, *args, **kwargs): |
|
175 | def _before(self, *args, **kwargs): | |
290 | super(ChangesetController, self)._before(*args, **kwargs) |
|
176 | super(ChangesetController, self)._before(*args, **kwargs) | |
@@ -292,17 +178,12 b' class ChangesetController(BaseRepoContro' | |||||
292 |
|
178 | |||
293 | def _index(self, revision, method): |
|
179 | def _index(self, revision, method): | |
294 | c.pull_request = None |
|
180 | c.pull_request = None | |
295 | c.anchor_url = anchor_url |
|
|||
296 | c.ignorews_url = _ignorews_url |
|
|||
297 | c.context_url = _context_url |
|
|||
298 | c.fulldiff = request.GET.get('fulldiff') # for reporting number of changed files |
|
181 | c.fulldiff = request.GET.get('fulldiff') # for reporting number of changed files | |
299 | # get ranges of revisions if preset |
|
182 | # get ranges of revisions if preset | |
300 | rev_range = revision.split('...')[:2] |
|
183 | rev_range = revision.split('...')[:2] | |
301 | enable_comments = True |
|
|||
302 | c.cs_repo = c.db_repo |
|
184 | c.cs_repo = c.db_repo | |
303 | try: |
|
185 | try: | |
304 | if len(rev_range) == 2: |
|
186 | if len(rev_range) == 2: | |
305 | enable_comments = False |
|
|||
306 | rev_start = rev_range[0] |
|
187 | rev_start = rev_range[0] | |
307 | rev_end = rev_range[1] |
|
188 | rev_end = rev_range[1] | |
308 | rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start, |
|
189 | rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start, | |
@@ -317,7 +198,7 b' class ChangesetController(BaseRepoContro' | |||||
317 | except (ChangesetDoesNotExistError, EmptyRepositoryError): |
|
198 | except (ChangesetDoesNotExistError, EmptyRepositoryError): | |
318 | log.debug(traceback.format_exc()) |
|
199 | log.debug(traceback.format_exc()) | |
319 | msg = _('Such revision does not exist for this repository') |
|
200 | msg = _('Such revision does not exist for this repository') | |
320 |
|
|
201 | webutils.flash(msg, category='error') | |
321 | raise HTTPNotFound() |
|
202 | raise HTTPNotFound() | |
322 |
|
203 | |||
323 | c.changes = OrderedDict() |
|
204 | c.changes = OrderedDict() | |
@@ -325,7 +206,7 b' class ChangesetController(BaseRepoContro' | |||||
325 | c.lines_added = 0 # count of lines added |
|
206 | c.lines_added = 0 # count of lines added | |
326 | c.lines_deleted = 0 # count of lines removes |
|
207 | c.lines_deleted = 0 # count of lines removes | |
327 |
|
208 | |||
328 | c.changeset_statuses = ChangesetStatus.STATUSES |
|
209 | c.changeset_statuses = db.ChangesetStatus.STATUSES | |
329 | comments = dict() |
|
210 | comments = dict() | |
330 | c.statuses = [] |
|
211 | c.statuses = [] | |
331 | c.inline_comments = [] |
|
212 | c.inline_comments = [] | |
@@ -357,11 +238,10 b' class ChangesetController(BaseRepoContro' | |||||
357 |
|
238 | |||
358 | cs2 = changeset.raw_id |
|
239 | cs2 = changeset.raw_id | |
359 | cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id |
|
240 | cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id | |
360 |
|
|
241 | ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET) | |
361 |
|
|
242 | diff_context_size = h.get_diff_context_size(request.GET) | |
362 |
|
||||
363 | raw_diff = diffs.get_diff(c.db_repo_scm_instance, cs1, cs2, |
|
243 | raw_diff = diffs.get_diff(c.db_repo_scm_instance, cs1, cs2, | |
364 |
ignore_whitespace=ign_whitespace_ |
|
244 | ignore_whitespace=ignore_whitespace_diff, context=diff_context_size) | |
365 | diff_limit = None if c.fulldiff else self.cut_off_limit |
|
245 | diff_limit = None if c.fulldiff else self.cut_off_limit | |
366 | file_diff_data = [] |
|
246 | file_diff_data = [] | |
367 | if method == 'show': |
|
247 | if method == 'show': | |
@@ -376,7 +256,7 b' class ChangesetController(BaseRepoContro' | |||||
376 | filename = f['filename'] |
|
256 | filename = f['filename'] | |
377 | fid = h.FID(changeset.raw_id, filename) |
|
257 | fid = h.FID(changeset.raw_id, filename) | |
378 | url_fid = h.FID('', filename) |
|
258 | url_fid = h.FID('', filename) | |
379 |
html_diff = diffs.as_html( |
|
259 | html_diff = diffs.as_html(parsed_lines=[f]) | |
380 | file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, html_diff, st)) |
|
260 | file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, html_diff, st)) | |
381 | else: |
|
261 | else: | |
382 | # downloads/raw we only need RAW diff nothing else |
|
262 | # downloads/raw we only need RAW diff nothing else | |
@@ -405,19 +285,19 b' class ChangesetController(BaseRepoContro' | |||||
405 | elif method == 'patch': |
|
285 | elif method == 'patch': | |
406 | response.content_type = 'text/plain' |
|
286 | response.content_type = 'text/plain' | |
407 | c.diff = safe_str(raw_diff) |
|
287 | c.diff = safe_str(raw_diff) | |
408 | return render('changeset/patch_changeset.html') |
|
288 | return base.render('changeset/patch_changeset.html') | |
409 | elif method == 'raw': |
|
289 | elif method == 'raw': | |
410 | response.content_type = 'text/plain' |
|
290 | response.content_type = 'text/plain' | |
411 | return raw_diff |
|
291 | return raw_diff | |
412 | elif method == 'show': |
|
292 | elif method == 'show': | |
413 | if len(c.cs_ranges) == 1: |
|
293 | if len(c.cs_ranges) == 1: | |
414 | return render('changeset/changeset.html') |
|
294 | return base.render('changeset/changeset.html') | |
415 | else: |
|
295 | else: | |
416 | c.cs_ranges_org = None |
|
296 | c.cs_ranges_org = None | |
417 | c.cs_comments = {} |
|
297 | c.cs_comments = {} | |
418 | revs = [ctx.revision for ctx in reversed(c.cs_ranges)] |
|
298 | revs = [ctx.revision for ctx in reversed(c.cs_ranges)] | |
419 | c.jsdata = graph_data(c.db_repo_scm_instance, revs) |
|
299 | c.jsdata = graph_data(c.db_repo_scm_instance, revs) | |
420 | return render('changeset/changeset_range.html') |
|
300 | return base.render('changeset/changeset_range.html') | |
421 |
|
301 | |||
422 | @LoginRequired(allow_default_user=True) |
|
302 | @LoginRequired(allow_default_user=True) | |
423 | @HasRepoPermissionLevelDecorator('read') |
|
303 | @HasRepoPermissionLevelDecorator('read') | |
@@ -441,19 +321,19 b' class ChangesetController(BaseRepoContro' | |||||
441 |
|
321 | |||
442 | @LoginRequired() |
|
322 | @LoginRequired() | |
443 | @HasRepoPermissionLevelDecorator('read') |
|
323 | @HasRepoPermissionLevelDecorator('read') | |
444 | @jsonify |
|
324 | @base.jsonify | |
445 | def comment(self, repo_name, revision): |
|
325 | def comment(self, repo_name, revision): | |
446 | return create_cs_pr_comment(repo_name, revision=revision) |
|
326 | return create_cs_pr_comment(repo_name, revision=revision) | |
447 |
|
327 | |||
448 | @LoginRequired() |
|
328 | @LoginRequired() | |
449 | @HasRepoPermissionLevelDecorator('read') |
|
329 | @HasRepoPermissionLevelDecorator('read') | |
450 | @jsonify |
|
330 | @base.jsonify | |
451 | def delete_comment(self, repo_name, comment_id): |
|
331 | def delete_comment(self, repo_name, comment_id): | |
452 | return delete_cs_pr_comment(repo_name, comment_id) |
|
332 | return delete_cs_pr_comment(repo_name, comment_id) | |
453 |
|
333 | |||
454 | @LoginRequired(allow_default_user=True) |
|
334 | @LoginRequired(allow_default_user=True) | |
455 | @HasRepoPermissionLevelDecorator('read') |
|
335 | @HasRepoPermissionLevelDecorator('read') | |
456 | @jsonify |
|
336 | @base.jsonify | |
457 | def changeset_info(self, repo_name, revision): |
|
337 | def changeset_info(self, repo_name, revision): | |
458 | if request.is_xhr: |
|
338 | if request.is_xhr: | |
459 | try: |
|
339 | try: | |
@@ -465,7 +345,7 b' class ChangesetController(BaseRepoContro' | |||||
465 |
|
345 | |||
466 | @LoginRequired(allow_default_user=True) |
|
346 | @LoginRequired(allow_default_user=True) | |
467 | @HasRepoPermissionLevelDecorator('read') |
|
347 | @HasRepoPermissionLevelDecorator('read') | |
468 | @jsonify |
|
348 | @base.jsonify | |
469 | def changeset_children(self, repo_name, revision): |
|
349 | def changeset_children(self, repo_name, revision): | |
470 | if request.is_xhr: |
|
350 | if request.is_xhr: | |
471 | changeset = c.db_repo_scm_instance.get_changeset(revision) |
|
351 | changeset = c.db_repo_scm_instance.get_changeset(revision) | |
@@ -478,7 +358,7 b' class ChangesetController(BaseRepoContro' | |||||
478 |
|
358 | |||
479 | @LoginRequired(allow_default_user=True) |
|
359 | @LoginRequired(allow_default_user=True) | |
480 | @HasRepoPermissionLevelDecorator('read') |
|
360 | @HasRepoPermissionLevelDecorator('read') | |
481 | @jsonify |
|
361 | @base.jsonify | |
482 | def changeset_parents(self, repo_name, revision): |
|
362 | def changeset_parents(self, repo_name, revision): | |
483 | if request.is_xhr: |
|
363 | if request.is_xhr: | |
484 | changeset = c.db_repo_scm_instance.get_changeset(revision) |
|
364 | changeset = c.db_repo_scm_instance.get_changeset(revision) |
@@ -28,29 +28,25 b' Original author and date, and relevant c' | |||||
28 |
|
28 | |||
29 |
|
29 | |||
30 | import logging |
|
30 | import logging | |
31 | import re |
|
|||
32 |
|
31 | |||
33 | import mercurial.unionrepo |
|
|||
34 | from tg import request |
|
32 | from tg import request | |
35 | from tg import tmpl_context as c |
|
33 | from tg import tmpl_context as c | |
36 | from tg.i18n import ugettext as _ |
|
34 | from tg.i18n import ugettext as _ | |
37 | from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound |
|
35 | from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound | |
38 |
|
36 | |||
39 | from kallithea.config.routing import url |
|
37 | import kallithea.lib.helpers as h | |
40 |
from kallithea.controllers |
|
38 | from kallithea.controllers import base | |
41 | from kallithea.lib import diffs |
|
39 | from kallithea.lib import diffs, webutils | |
42 | from kallithea.lib import helpers as h |
|
|||
43 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
40 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
44 | from kallithea.lib.base import BaseRepoController, render |
|
|||
45 | from kallithea.lib.graphmod import graph_data |
|
41 | from kallithea.lib.graphmod import graph_data | |
46 | from kallithea.lib.utils2 import ascii_bytes, ascii_str, safe_bytes, safe_int |
|
42 | from kallithea.lib.webutils import url | |
47 |
from kallithea.model |
|
43 | from kallithea.model import db | |
48 |
|
44 | |||
49 |
|
45 | |||
50 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
51 |
|
47 | |||
52 |
|
48 | |||
53 | class CompareController(BaseRepoController): |
|
49 | class CompareController(base.BaseRepoController): | |
54 |
|
50 | |||
55 | def _before(self, *args, **kwargs): |
|
51 | def _before(self, *args, **kwargs): | |
56 | super(CompareController, self)._before(*args, **kwargs) |
|
52 | super(CompareController, self)._before(*args, **kwargs) | |
@@ -63,122 +59,24 b' class CompareController(BaseRepoControll' | |||||
63 | if other_repo is None: |
|
59 | if other_repo is None: | |
64 | c.cs_repo = c.a_repo |
|
60 | c.cs_repo = c.a_repo | |
65 | else: |
|
61 | else: | |
66 | c.cs_repo = Repository.get_by_repo_name(other_repo) |
|
62 | c.cs_repo = db.Repository.get_by_repo_name(other_repo) | |
67 | if c.cs_repo is None: |
|
63 | if c.cs_repo is None: | |
68 | msg = _('Could not find other repository %s') % other_repo |
|
64 | msg = _('Could not find other repository %s') % other_repo | |
69 |
|
|
65 | webutils.flash(msg, category='error') | |
70 | raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name)) |
|
66 | raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name)) | |
71 |
|
67 | |||
72 | # Verify that it's even possible to compare these two repositories. |
|
68 | # Verify that it's even possible to compare these two repositories. | |
73 | if c.a_repo.scm_instance.alias != c.cs_repo.scm_instance.alias: |
|
69 | if c.a_repo.scm_instance.alias != c.cs_repo.scm_instance.alias: | |
74 | msg = _('Cannot compare repositories of different types') |
|
70 | msg = _('Cannot compare repositories of different types') | |
75 |
|
|
71 | webutils.flash(msg, category='error') | |
76 | raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name)) |
|
72 | raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name)) | |
77 |
|
73 | |||
78 | @staticmethod |
|
|||
79 | def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev): |
|
|||
80 | """ |
|
|||
81 | Returns lists of changesets that can be merged from org_repo@org_rev |
|
|||
82 | to other_repo@other_rev |
|
|||
83 | ... and the other way |
|
|||
84 | ... and the ancestors that would be used for merge |
|
|||
85 |
|
||||
86 | :param org_repo: repo object, that is most likely the original repo we forked from |
|
|||
87 | :param org_rev: the revision we want our compare to be made |
|
|||
88 | :param other_repo: repo object, most likely the fork of org_repo. It has |
|
|||
89 | all changesets that we need to obtain |
|
|||
90 | :param other_rev: revision we want out compare to be made on other_repo |
|
|||
91 | """ |
|
|||
92 | ancestors = None |
|
|||
93 | if org_rev == other_rev: |
|
|||
94 | org_changesets = [] |
|
|||
95 | other_changesets = [] |
|
|||
96 |
|
||||
97 | elif alias == 'hg': |
|
|||
98 | # case two independent repos |
|
|||
99 | if org_repo != other_repo: |
|
|||
100 | hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui, |
|
|||
101 | safe_bytes(other_repo.path), |
|
|||
102 | safe_bytes(org_repo.path)) |
|
|||
103 | # all ancestors of other_rev will be in other_repo and |
|
|||
104 | # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot |
|
|||
105 |
|
||||
106 | # no remote compare do it on the same repository |
|
|||
107 | else: |
|
|||
108 | hgrepo = other_repo._repo |
|
|||
109 |
|
||||
110 | ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in |
|
|||
111 | hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))] |
|
|||
112 | if ancestors: |
|
|||
113 | log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev) |
|
|||
114 | else: |
|
|||
115 | log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev) |
|
|||
116 | ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in |
|
|||
117 | hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive! |
|
|||
118 |
|
||||
119 | other_changesets = [ |
|
|||
120 | other_repo.get_changeset(rev) |
|
|||
121 | for rev in hgrepo.revs( |
|
|||
122 | b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)", |
|
|||
123 | ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev)) |
|
|||
124 | ] |
|
|||
125 | org_changesets = [ |
|
|||
126 | org_repo.get_changeset(ascii_str(hgrepo[rev].hex())) |
|
|||
127 | for rev in hgrepo.revs( |
|
|||
128 | b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)", |
|
|||
129 | ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev)) |
|
|||
130 | ] |
|
|||
131 |
|
||||
132 | elif alias == 'git': |
|
|||
133 | if org_repo != other_repo: |
|
|||
134 | from dulwich.repo import Repo |
|
|||
135 | from dulwich.client import SubprocessGitClient |
|
|||
136 |
|
||||
137 | gitrepo = Repo(org_repo.path) |
|
|||
138 | SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo) |
|
|||
139 |
|
||||
140 | gitrepo_remote = Repo(other_repo.path) |
|
|||
141 | SubprocessGitClient(thin_packs=False).fetch(org_repo.path, gitrepo_remote) |
|
|||
142 |
|
||||
143 | revs = [ |
|
|||
144 | ascii_str(x.commit.id) |
|
|||
145 | for x in gitrepo_remote.get_walker(include=[ascii_bytes(other_rev)], |
|
|||
146 | exclude=[ascii_bytes(org_rev)]) |
|
|||
147 | ] |
|
|||
148 | other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)] |
|
|||
149 | if other_changesets: |
|
|||
150 | ancestors = [other_changesets[0].parents[0].raw_id] |
|
|||
151 | else: |
|
|||
152 | # no changesets from other repo, ancestor is the other_rev |
|
|||
153 | ancestors = [other_rev] |
|
|||
154 |
|
||||
155 | gitrepo.close() |
|
|||
156 | gitrepo_remote.close() |
|
|||
157 |
|
||||
158 | else: |
|
|||
159 | so = org_repo.run_git_command( |
|
|||
160 | ['log', '--reverse', '--pretty=format:%H', |
|
|||
161 | '-s', '%s..%s' % (org_rev, other_rev)] |
|
|||
162 | ) |
|
|||
163 | other_changesets = [org_repo.get_changeset(cs) |
|
|||
164 | for cs in re.findall(r'[0-9a-fA-F]{40}', so)] |
|
|||
165 | so = org_repo.run_git_command( |
|
|||
166 | ['merge-base', org_rev, other_rev] |
|
|||
167 | ) |
|
|||
168 | ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]] |
|
|||
169 | org_changesets = [] |
|
|||
170 |
|
||||
171 | else: |
|
|||
172 | raise Exception('Bad alias only git and hg is allowed') |
|
|||
173 |
|
||||
174 | return other_changesets, org_changesets, ancestors |
|
|||
175 |
|
||||
176 | @LoginRequired(allow_default_user=True) |
|
74 | @LoginRequired(allow_default_user=True) | |
177 | @HasRepoPermissionLevelDecorator('read') |
|
75 | @HasRepoPermissionLevelDecorator('read') | |
178 | def index(self, repo_name): |
|
76 | def index(self, repo_name): | |
179 | c.compare_home = True |
|
77 | c.compare_home = True | |
180 | c.a_ref_name = c.cs_ref_name = None |
|
78 | c.a_ref_name = c.cs_ref_name = None | |
181 | return render('compare/compare_diff.html') |
|
79 | return base.render('compare/compare_diff.html') | |
182 |
|
80 | |||
183 | @LoginRequired(allow_default_user=True) |
|
81 | @LoginRequired(allow_default_user=True) | |
184 | @HasRepoPermissionLevelDecorator('read') |
|
82 | @HasRepoPermissionLevelDecorator('read') | |
@@ -202,18 +100,14 b' class CompareController(BaseRepoControll' | |||||
202 | # is_ajax_preview puts hidden input field with changeset revisions |
|
100 | # is_ajax_preview puts hidden input field with changeset revisions | |
203 | c.is_ajax_preview = partial and request.GET.get('is_ajax_preview') |
|
101 | c.is_ajax_preview = partial and request.GET.get('is_ajax_preview') | |
204 | # swap url for compare_diff page - never partial and never is_ajax_preview |
|
102 | # swap url for compare_diff page - never partial and never is_ajax_preview | |
205 |
c.swap_url = |
|
103 | c.swap_url = webutils.url('compare_url', | |
206 | repo_name=c.cs_repo.repo_name, |
|
104 | repo_name=c.cs_repo.repo_name, | |
207 | org_ref_type=other_ref_type, org_ref_name=other_ref_name, |
|
105 | org_ref_type=other_ref_type, org_ref_name=other_ref_name, | |
208 | other_repo=c.a_repo.repo_name, |
|
106 | other_repo=c.a_repo.repo_name, | |
209 | other_ref_type=org_ref_type, other_ref_name=org_ref_name, |
|
107 | other_ref_type=org_ref_type, other_ref_name=org_ref_name, | |
210 | merge=merge or '') |
|
108 | merge=merge or '') | |
211 |
|
109 | ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET) | ||
212 | # set callbacks for generating markup for icons |
|
110 | diff_context_size = h.get_diff_context_size(request.GET) | |
213 | c.ignorews_url = _ignorews_url |
|
|||
214 | c.context_url = _context_url |
|
|||
215 | ignore_whitespace = request.GET.get('ignorews') == '1' |
|
|||
216 | line_context = safe_int(request.GET.get('context'), 3) |
|
|||
217 |
|
111 | |||
218 | c.a_rev = self._get_ref_rev(c.a_repo, org_ref_type, org_ref_name, |
|
112 | c.a_rev = self._get_ref_rev(c.a_repo, org_ref_type, org_ref_name, | |
219 | returnempty=True) |
|
113 | returnempty=True) | |
@@ -225,9 +119,8 b' class CompareController(BaseRepoControll' | |||||
225 | c.cs_ref_name = other_ref_name |
|
119 | c.cs_ref_name = other_ref_name | |
226 | c.cs_ref_type = other_ref_type |
|
120 | c.cs_ref_type = other_ref_type | |
227 |
|
121 | |||
228 |
c.cs_ranges, c.cs_ranges_org, c.ancestors = |
|
122 | c.cs_ranges, c.cs_ranges_org, c.ancestors = c.a_repo.scm_instance.get_diff_changesets( | |
229 |
c.a_re |
|
123 | c.a_rev, c.cs_repo.scm_instance, c.cs_rev) | |
230 | c.cs_repo.scm_instance, c.cs_rev) |
|
|||
231 | raw_ids = [x.raw_id for x in c.cs_ranges] |
|
124 | raw_ids = [x.raw_id for x in c.cs_ranges] | |
232 | c.cs_comments = c.cs_repo.get_comments(raw_ids) |
|
125 | c.cs_comments = c.cs_repo.get_comments(raw_ids) | |
233 | c.cs_statuses = c.cs_repo.statuses(raw_ids) |
|
126 | c.cs_statuses = c.cs_repo.statuses(raw_ids) | |
@@ -236,7 +129,7 b' class CompareController(BaseRepoControll' | |||||
236 | c.jsdata = graph_data(c.cs_repo.scm_instance, revs) |
|
129 | c.jsdata = graph_data(c.cs_repo.scm_instance, revs) | |
237 |
|
130 | |||
238 | if partial: |
|
131 | if partial: | |
239 | return render('compare/compare_cs.html') |
|
132 | return base.render('compare/compare_cs.html') | |
240 |
|
133 | |||
241 | org_repo = c.a_repo |
|
134 | org_repo = c.a_repo | |
242 | other_repo = c.cs_repo |
|
135 | other_repo = c.cs_repo | |
@@ -252,7 +145,7 b' class CompareController(BaseRepoControll' | |||||
252 | else: |
|
145 | else: | |
253 | msg = _('Multiple merge ancestors found for merge compare') |
|
146 | msg = _('Multiple merge ancestors found for merge compare') | |
254 | if rev1 is None: |
|
147 | if rev1 is None: | |
255 |
|
|
148 | webutils.flash(msg, category='error') | |
256 | log.error(msg) |
|
149 | log.error(msg) | |
257 | raise HTTPNotFound |
|
150 | raise HTTPNotFound | |
258 |
|
151 | |||
@@ -266,7 +159,7 b' class CompareController(BaseRepoControll' | |||||
266 | if org_repo != other_repo: |
|
159 | if org_repo != other_repo: | |
267 | # TODO: we could do this by using hg unionrepo |
|
160 | # TODO: we could do this by using hg unionrepo | |
268 | log.error('cannot compare across repos %s and %s', org_repo, other_repo) |
|
161 | log.error('cannot compare across repos %s and %s', org_repo, other_repo) | |
269 |
|
|
162 | webutils.flash(_('Cannot compare repositories without using common ancestor'), category='error') | |
270 | raise HTTPBadRequest |
|
163 | raise HTTPBadRequest | |
271 | rev1 = c.a_rev |
|
164 | rev1 = c.a_rev | |
272 |
|
165 | |||
@@ -275,8 +168,8 b' class CompareController(BaseRepoControll' | |||||
275 | log.debug('running diff between %s and %s in %s', |
|
168 | log.debug('running diff between %s and %s in %s', | |
276 | rev1, c.cs_rev, org_repo.scm_instance.path) |
|
169 | rev1, c.cs_rev, org_repo.scm_instance.path) | |
277 | raw_diff = diffs.get_diff(org_repo.scm_instance, rev1=rev1, rev2=c.cs_rev, |
|
170 | raw_diff = diffs.get_diff(org_repo.scm_instance, rev1=rev1, rev2=c.cs_rev, | |
278 | ignore_whitespace=ignore_whitespace, |
|
171 | ignore_whitespace=ignore_whitespace_diff, | |
279 |
context= |
|
172 | context=diff_context_size) | |
280 |
|
173 | |||
281 | diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit) |
|
174 | diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit) | |
282 | c.limited_diff = diff_processor.limited_diff |
|
175 | c.limited_diff = diff_processor.limited_diff | |
@@ -289,7 +182,7 b' class CompareController(BaseRepoControll' | |||||
289 | c.lines_deleted += st['deleted'] |
|
182 | c.lines_deleted += st['deleted'] | |
290 | filename = f['filename'] |
|
183 | filename = f['filename'] | |
291 | fid = h.FID('', filename) |
|
184 | fid = h.FID('', filename) | |
292 |
html_diff = diffs.as_html( |
|
185 | html_diff = diffs.as_html(parsed_lines=[f]) | |
293 | c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st)) |
|
186 | c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st)) | |
294 |
|
187 | |||
295 | return render('compare/compare_diff.html') |
|
188 | return base.render('compare/compare_diff.html') |
@@ -32,13 +32,13 b' from tg import config, expose, request' | |||||
32 | from tg import tmpl_context as c |
|
32 | from tg import tmpl_context as c | |
33 | from tg.i18n import ugettext as _ |
|
33 | from tg.i18n import ugettext as _ | |
34 |
|
34 | |||
35 | from kallithea.lib.base import BaseController |
|
35 | from kallithea.controllers import base | |
36 |
|
36 | |||
37 |
|
37 | |||
38 | log = logging.getLogger(__name__) |
|
38 | log = logging.getLogger(__name__) | |
39 |
|
39 | |||
40 |
|
40 | |||
41 | class ErrorController(BaseController): |
|
41 | class ErrorController(base.BaseController): | |
42 | """Generates error documents as and when they are required. |
|
42 | """Generates error documents as and when they are required. | |
43 |
|
43 | |||
44 | The errorpage middleware renders /error/document when error |
|
44 | The errorpage middleware renders /error/document when error |
@@ -33,19 +33,19 b' from tg import response' | |||||
33 | from tg import tmpl_context as c |
|
33 | from tg import tmpl_context as c | |
34 | from tg.i18n import ugettext as _ |
|
34 | from tg.i18n import ugettext as _ | |
35 |
|
35 | |||
36 | from kallithea import CONFIG |
|
36 | import kallithea | |
37 | from kallithea.lib import feeds |
|
37 | import kallithea.lib.helpers as h | |
38 |
from kallithea. |
|
38 | from kallithea.controllers import base | |
|
39 | from kallithea.lib import feeds, webutils | |||
39 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
40 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
40 | from kallithea.lib.base import BaseRepoController |
|
|||
41 | from kallithea.lib.diffs import DiffProcessor |
|
41 | from kallithea.lib.diffs import DiffProcessor | |
42 |
from kallithea.lib.utils2 import safe_int, safe_str |
|
42 | from kallithea.lib.utils2 import asbool, safe_int, safe_str | |
43 |
|
43 | |||
44 |
|
44 | |||
45 | log = logging.getLogger(__name__) |
|
45 | log = logging.getLogger(__name__) | |
46 |
|
46 | |||
47 |
|
47 | |||
48 | class FeedController(BaseRepoController): |
|
48 | class FeedController(base.BaseRepoController): | |
49 |
|
49 | |||
50 | @LoginRequired(allow_default_user=True) |
|
50 | @LoginRequired(allow_default_user=True) | |
51 | @HasRepoPermissionLevelDecorator('read') |
|
51 | @HasRepoPermissionLevelDecorator('read') | |
@@ -53,11 +53,11 b' class FeedController(BaseRepoController)' | |||||
53 | super(FeedController, self)._before(*args, **kwargs) |
|
53 | super(FeedController, self)._before(*args, **kwargs) | |
54 |
|
54 | |||
55 | def _get_title(self, cs): |
|
55 | def _get_title(self, cs): | |
56 |
return |
|
56 | return webutils.shorter(cs.message, 160) | |
57 |
|
57 | |||
58 | def __get_desc(self, cs): |
|
58 | def __get_desc(self, cs): | |
59 | desc_msg = [(_('%s committed on %s') |
|
59 | desc_msg = [(_('%s committed on %s') | |
60 |
% (h.person(cs.author), |
|
60 | % (h.person(cs.author), webutils.fmt_date(cs.date))) + '<br/>'] | |
61 | # branches, tags, bookmarks |
|
61 | # branches, tags, bookmarks | |
62 | for branch in cs.branches: |
|
62 | for branch in cs.branches: | |
63 | desc_msg.append('branch: %s<br/>' % branch) |
|
63 | desc_msg.append('branch: %s<br/>' % branch) | |
@@ -67,11 +67,11 b' class FeedController(BaseRepoController)' | |||||
67 | desc_msg.append('tag: %s<br/>' % tag) |
|
67 | desc_msg.append('tag: %s<br/>' % tag) | |
68 |
|
68 | |||
69 | changes = [] |
|
69 | changes = [] | |
70 | diff_limit = safe_int(CONFIG.get('rss_cut_off_limit', 32 * 1024)) |
|
70 | diff_limit = safe_int(kallithea.CONFIG.get('rss_cut_off_limit', 32 * 1024)) | |
71 | raw_diff = cs.diff() |
|
71 | raw_diff = cs.diff() | |
72 | diff_processor = DiffProcessor(raw_diff, |
|
72 | diff_processor = DiffProcessor(raw_diff, | |
73 | diff_limit=diff_limit, |
|
73 | diff_limit=diff_limit, | |
74 |
|
|
74 | html=False) | |
75 |
|
75 | |||
76 | for st in diff_processor.parsed: |
|
76 | for st in diff_processor.parsed: | |
77 | st.update({'added': st['stats']['added'], |
|
77 | st.update({'added': st['stats']['added'], | |
@@ -84,15 +84,15 b' class FeedController(BaseRepoController)' | |||||
84 | _('Changeset was too big and was cut off...')] |
|
84 | _('Changeset was too big and was cut off...')] | |
85 |
|
85 | |||
86 | # rev link |
|
86 | # rev link | |
87 |
_url = |
|
87 | _url = webutils.canonical_url('changeset_home', repo_name=c.db_repo.repo_name, | |
88 | revision=cs.raw_id) |
|
88 | revision=cs.raw_id) | |
89 | desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8])) |
|
89 | desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8])) | |
90 |
|
90 | |||
91 | desc_msg.append('<pre>') |
|
91 | desc_msg.append('<pre>') | |
92 |
desc_msg.append( |
|
92 | desc_msg.append(webutils.urlify_text(cs.message)) | |
93 | desc_msg.append('\n') |
|
93 | desc_msg.append('\n') | |
94 | desc_msg.extend(changes) |
|
94 | desc_msg.extend(changes) | |
95 |
if s |
|
95 | if asbool(kallithea.CONFIG.get('rss_include_diff', False)): | |
96 | desc_msg.append('\n\n') |
|
96 | desc_msg.append('\n\n') | |
97 | desc_msg.append(safe_str(raw_diff)) |
|
97 | desc_msg.append(safe_str(raw_diff)) | |
98 | desc_msg.append('</pre>') |
|
98 | desc_msg.append('</pre>') | |
@@ -105,16 +105,16 b' class FeedController(BaseRepoController)' | |||||
105 | def _get_feed_from_cache(*_cache_keys): # parameters are not really used - only as caching key |
|
105 | def _get_feed_from_cache(*_cache_keys): # parameters are not really used - only as caching key | |
106 | header = dict( |
|
106 | header = dict( | |
107 | title=_('%s %s feed') % (c.site_name, repo_name), |
|
107 | title=_('%s %s feed') % (c.site_name, repo_name), | |
108 |
link= |
|
108 | link=webutils.canonical_url('summary_home', repo_name=repo_name), | |
109 | description=_('Changes on %s repository') % repo_name, |
|
109 | description=_('Changes on %s repository') % repo_name, | |
110 | ) |
|
110 | ) | |
111 |
|
111 | |||
112 | rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20)) |
|
112 | rss_items_per_page = safe_int(kallithea.CONFIG.get('rss_items_per_page', 20)) | |
113 | entries=[] |
|
113 | entries=[] | |
114 | for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])): |
|
114 | for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])): | |
115 | entries.append(dict( |
|
115 | entries.append(dict( | |
116 | title=self._get_title(cs), |
|
116 | title=self._get_title(cs), | |
117 |
link= |
|
117 | link=webutils.canonical_url('changeset_home', repo_name=repo_name, revision=cs.raw_id), | |
118 | author_email=cs.author_email, |
|
118 | author_email=cs.author_email, | |
119 | author_name=cs.author_name, |
|
119 | author_name=cs.author_name, | |
120 | description=''.join(self.__get_desc(cs)), |
|
120 | description=''.join(self.__get_desc(cs)), |
@@ -38,21 +38,21 b' from tg import tmpl_context as c' | |||||
38 | from tg.i18n import ugettext as _ |
|
38 | from tg.i18n import ugettext as _ | |
39 | from webob.exc import HTTPFound, HTTPNotFound |
|
39 | from webob.exc import HTTPFound, HTTPNotFound | |
40 |
|
40 | |||
41 | from kallithea.config.routing import url |
|
41 | import kallithea | |
42 | from kallithea.controllers.changeset import _context_url, _ignorews_url, anchor_url, get_ignore_ws, get_line_ctx |
|
42 | import kallithea.lib.helpers as h | |
43 |
from kallithea. |
|
43 | from kallithea.controllers import base | |
44 |
from kallithea.lib import |
|
44 | from kallithea.lib import diffs, webutils | |
45 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
45 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
46 | from kallithea.lib.base import BaseRepoController, jsonify, render |
|
|||
47 | from kallithea.lib.exceptions import NonRelativePathError |
|
46 | from kallithea.lib.exceptions import NonRelativePathError | |
48 | from kallithea.lib.utils import action_logger |
|
47 | from kallithea.lib.utils2 import asbool, convert_line_endings, detect_mode, safe_str | |
49 | from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_int, safe_str, str2bool |
|
|||
50 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
48 | from kallithea.lib.vcs.backends.base import EmptyChangeset | |
51 | from kallithea.lib.vcs.conf import settings |
|
49 | from kallithea.lib.vcs.conf import settings | |
52 | from kallithea.lib.vcs.exceptions import (ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, ImproperArchiveTypeError, NodeAlreadyExistsError, |
|
50 | from kallithea.lib.vcs.exceptions import (ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, ImproperArchiveTypeError, NodeAlreadyExistsError, | |
53 | NodeDoesNotExistError, NodeError, RepositoryError, VCSError) |
|
51 | NodeDoesNotExistError, NodeError, RepositoryError, VCSError) | |
54 | from kallithea.lib.vcs.nodes import FileNode |
|
52 | from kallithea.lib.vcs.nodes import FileNode | |
55 |
from kallithea. |
|
53 | from kallithea.lib.vcs.utils import author_email | |
|
54 | from kallithea.lib.webutils import url | |||
|
55 | from kallithea.model import userlog | |||
56 | from kallithea.model.repo import RepoModel |
|
56 | from kallithea.model.repo import RepoModel | |
57 | from kallithea.model.scm import ScmModel |
|
57 | from kallithea.model.scm import ScmModel | |
58 |
|
58 | |||
@@ -60,7 +60,7 b' from kallithea.model.scm import ScmModel' | |||||
60 | log = logging.getLogger(__name__) |
|
60 | log = logging.getLogger(__name__) | |
61 |
|
61 | |||
62 |
|
62 | |||
63 | class FilesController(BaseRepoController): |
|
63 | class FilesController(base.BaseRepoController): | |
64 |
|
64 | |||
65 | def _before(self, *args, **kwargs): |
|
65 | def _before(self, *args, **kwargs): | |
66 | super(FilesController, self)._before(*args, **kwargs) |
|
66 | super(FilesController, self)._before(*args, **kwargs) | |
@@ -82,15 +82,15 b' class FilesController(BaseRepoController' | |||||
82 | url_ = url('files_add_home', |
|
82 | url_ = url('files_add_home', | |
83 | repo_name=c.repo_name, |
|
83 | repo_name=c.repo_name, | |
84 | revision=0, f_path='', anchor='edit') |
|
84 | revision=0, f_path='', anchor='edit') | |
85 |
add_new = |
|
85 | add_new = webutils.link_to(_('Click here to add new file'), url_, class_="alert-link") | |
86 |
|
|
86 | webutils.flash(_('There are no files yet.') + ' ' + add_new, category='warning') | |
87 | raise HTTPNotFound() |
|
87 | raise HTTPNotFound() | |
88 | except (ChangesetDoesNotExistError, LookupError): |
|
88 | except (ChangesetDoesNotExistError, LookupError): | |
89 | msg = _('Such revision does not exist for this repository') |
|
89 | msg = _('Such revision does not exist for this repository') | |
90 |
|
|
90 | webutils.flash(msg, category='error') | |
91 | raise HTTPNotFound() |
|
91 | raise HTTPNotFound() | |
92 | except RepositoryError as e: |
|
92 | except RepositoryError as e: | |
93 |
|
|
93 | webutils.flash(e, category='error') | |
94 | raise HTTPNotFound() |
|
94 | raise HTTPNotFound() | |
95 |
|
95 | |||
96 | def __get_filenode(self, cs, path): |
|
96 | def __get_filenode(self, cs, path): | |
@@ -107,10 +107,10 b' class FilesController(BaseRepoController' | |||||
107 | raise RepositoryError('given path is a directory') |
|
107 | raise RepositoryError('given path is a directory') | |
108 | except ChangesetDoesNotExistError: |
|
108 | except ChangesetDoesNotExistError: | |
109 | msg = _('Such revision does not exist for this repository') |
|
109 | msg = _('Such revision does not exist for this repository') | |
110 |
|
|
110 | webutils.flash(msg, category='error') | |
111 | raise HTTPNotFound() |
|
111 | raise HTTPNotFound() | |
112 | except RepositoryError as e: |
|
112 | except RepositoryError as e: | |
113 |
|
|
113 | webutils.flash(e, category='error') | |
114 | raise HTTPNotFound() |
|
114 | raise HTTPNotFound() | |
115 |
|
115 | |||
116 | return file_node |
|
116 | return file_node | |
@@ -171,30 +171,30 b' class FilesController(BaseRepoController' | |||||
171 |
|
171 | |||
172 | c.authors = [] |
|
172 | c.authors = [] | |
173 | for a in set([x.author for x in _hist]): |
|
173 | for a in set([x.author for x in _hist]): | |
174 |
c.authors.append(( |
|
174 | c.authors.append((author_email(a), h.person(a))) | |
175 | else: |
|
175 | else: | |
176 | c.authors = c.file_history = [] |
|
176 | c.authors = c.file_history = [] | |
177 | except RepositoryError as e: |
|
177 | except RepositoryError as e: | |
178 |
|
|
178 | webutils.flash(e, category='error') | |
179 | raise HTTPNotFound() |
|
179 | raise HTTPNotFound() | |
180 |
|
180 | |||
181 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
181 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
182 | return render('files/files_ypjax.html') |
|
182 | return base.render('files/files_ypjax.html') | |
183 |
|
183 | |||
184 | # TODO: tags and bookmarks? |
|
184 | # TODO: tags and bookmarks? | |
185 | c.revision_options = [(c.changeset.raw_id, |
|
185 | c.revision_options = [(c.changeset.raw_id, | |
186 |
_('%s at %s') % (b, |
|
186 | _('%s at %s') % (b, c.changeset.short_id)) for b in c.changeset.branches] + \ | |
187 | [(n, b) for b, n in c.db_repo_scm_instance.branches.items()] |
|
187 | [(n, b) for b, n in c.db_repo_scm_instance.branches.items()] | |
188 | if c.db_repo_scm_instance.closed_branches: |
|
188 | if c.db_repo_scm_instance.closed_branches: | |
189 | prefix = _('(closed)') + ' ' |
|
189 | prefix = _('(closed)') + ' ' | |
190 | c.revision_options += [('-', '-')] + \ |
|
190 | c.revision_options += [('-', '-')] + \ | |
191 | [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()] |
|
191 | [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()] | |
192 |
|
192 | |||
193 | return render('files/files.html') |
|
193 | return base.render('files/files.html') | |
194 |
|
194 | |||
195 | @LoginRequired(allow_default_user=True) |
|
195 | @LoginRequired(allow_default_user=True) | |
196 | @HasRepoPermissionLevelDecorator('read') |
|
196 | @HasRepoPermissionLevelDecorator('read') | |
197 | @jsonify |
|
197 | @base.jsonify | |
198 | def history(self, repo_name, revision, f_path): |
|
198 | def history(self, repo_name, revision, f_path): | |
199 | changeset = self.__get_cs(revision) |
|
199 | changeset = self.__get_cs(revision) | |
200 | _file = changeset.get_node(f_path) |
|
200 | _file = changeset.get_node(f_path) | |
@@ -223,8 +223,8 b' class FilesController(BaseRepoController' | |||||
223 | file_history, _hist = self._get_node_history(changeset, f_path) |
|
223 | file_history, _hist = self._get_node_history(changeset, f_path) | |
224 | c.authors = [] |
|
224 | c.authors = [] | |
225 | for a in set([x.author for x in _hist]): |
|
225 | for a in set([x.author for x in _hist]): | |
226 |
c.authors.append(( |
|
226 | c.authors.append((author_email(a), h.person(a))) | |
227 | return render('files/files_history_box.html') |
|
227 | return base.render('files/files_history_box.html') | |
228 |
|
228 | |||
229 | @LoginRequired(allow_default_user=True) |
|
229 | @LoginRequired(allow_default_user=True) | |
230 | @HasRepoPermissionLevelDecorator('read') |
|
230 | @HasRepoPermissionLevelDecorator('read') | |
@@ -233,7 +233,7 b' class FilesController(BaseRepoController' | |||||
233 | file_node = self.__get_filenode(cs, f_path) |
|
233 | file_node = self.__get_filenode(cs, f_path) | |
234 |
|
234 | |||
235 | response.content_disposition = \ |
|
235 | response.content_disposition = \ | |
236 |
'attachment; filename=%s' % f_path.split( |
|
236 | 'attachment; filename=%s' % f_path.split(kallithea.URL_SEP)[-1] | |
237 |
|
237 | |||
238 | response.content_type = file_node.mimetype |
|
238 | response.content_type = file_node.mimetype | |
239 | return file_node.content |
|
239 | return file_node.content | |
@@ -292,9 +292,9 b' class FilesController(BaseRepoController' | |||||
292 | _branches = repo.scm_instance.branches |
|
292 | _branches = repo.scm_instance.branches | |
293 | # check if revision is a branch name or branch hash |
|
293 | # check if revision is a branch name or branch hash | |
294 | if revision not in _branches and revision not in _branches.values(): |
|
294 | if revision not in _branches and revision not in _branches.values(): | |
295 |
|
|
295 | webutils.flash(_('You can only delete files with revision ' | |
296 | 'being a valid branch'), category='warning') |
|
296 | 'being a valid branch'), category='warning') | |
297 |
raise HTTPFound(location= |
|
297 | raise HTTPFound(location=webutils.url('files_home', | |
298 | repo_name=repo_name, revision='tip', |
|
298 | repo_name=repo_name, revision='tip', | |
299 | f_path=f_path)) |
|
299 | f_path=f_path)) | |
300 |
|
300 | |||
@@ -327,15 +327,15 b' class FilesController(BaseRepoController' | |||||
327 | author=author, |
|
327 | author=author, | |
328 | ) |
|
328 | ) | |
329 |
|
329 | |||
330 |
|
|
330 | webutils.flash(_('Successfully deleted file %s') % f_path, | |
331 | category='success') |
|
331 | category='success') | |
332 | except Exception: |
|
332 | except Exception: | |
333 | log.error(traceback.format_exc()) |
|
333 | log.error(traceback.format_exc()) | |
334 |
|
|
334 | webutils.flash(_('Error occurred during commit'), category='error') | |
335 | raise HTTPFound(location=url('changeset_home', |
|
335 | raise HTTPFound(location=url('changeset_home', | |
336 | repo_name=c.repo_name, revision='tip')) |
|
336 | repo_name=c.repo_name, revision='tip')) | |
337 |
|
337 | |||
338 | return render('files/files_delete.html') |
|
338 | return base.render('files/files_delete.html') | |
339 |
|
339 | |||
340 | @LoginRequired() |
|
340 | @LoginRequired() | |
341 | @HasRepoPermissionLevelDecorator('write') |
|
341 | @HasRepoPermissionLevelDecorator('write') | |
@@ -346,9 +346,9 b' class FilesController(BaseRepoController' | |||||
346 | _branches = repo.scm_instance.branches |
|
346 | _branches = repo.scm_instance.branches | |
347 | # check if revision is a branch name or branch hash |
|
347 | # check if revision is a branch name or branch hash | |
348 | if revision not in _branches and revision not in _branches.values(): |
|
348 | if revision not in _branches and revision not in _branches.values(): | |
349 |
|
|
349 | webutils.flash(_('You can only edit files with revision ' | |
350 | 'being a valid branch'), category='warning') |
|
350 | 'being a valid branch'), category='warning') | |
351 |
raise HTTPFound(location= |
|
351 | raise HTTPFound(location=webutils.url('files_home', | |
352 | repo_name=repo_name, revision='tip', |
|
352 | repo_name=repo_name, revision='tip', | |
353 | f_path=f_path)) |
|
353 | f_path=f_path)) | |
354 |
|
354 | |||
@@ -375,7 +375,7 b' class FilesController(BaseRepoController' | |||||
375 | author = request.authuser.full_contact |
|
375 | author = request.authuser.full_contact | |
376 |
|
376 | |||
377 | if content == old_content: |
|
377 | if content == old_content: | |
378 |
|
|
378 | webutils.flash(_('No changes'), category='warning') | |
379 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, |
|
379 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, | |
380 | revision='tip')) |
|
380 | revision='tip')) | |
381 | try: |
|
381 | try: | |
@@ -385,15 +385,15 b' class FilesController(BaseRepoController' | |||||
385 | ip_addr=request.ip_addr, |
|
385 | ip_addr=request.ip_addr, | |
386 | author=author, message=message, |
|
386 | author=author, message=message, | |
387 | content=content, f_path=f_path) |
|
387 | content=content, f_path=f_path) | |
388 |
|
|
388 | webutils.flash(_('Successfully committed to %s') % f_path, | |
389 | category='success') |
|
389 | category='success') | |
390 | except Exception: |
|
390 | except Exception: | |
391 | log.error(traceback.format_exc()) |
|
391 | log.error(traceback.format_exc()) | |
392 |
|
|
392 | webutils.flash(_('Error occurred during commit'), category='error') | |
393 | raise HTTPFound(location=url('changeset_home', |
|
393 | raise HTTPFound(location=url('changeset_home', | |
394 | repo_name=c.repo_name, revision='tip')) |
|
394 | repo_name=c.repo_name, revision='tip')) | |
395 |
|
395 | |||
396 | return render('files/files_edit.html') |
|
396 | return base.render('files/files_edit.html') | |
397 |
|
397 | |||
398 | @LoginRequired() |
|
398 | @LoginRequired() | |
399 | @HasRepoPermissionLevelDecorator('write') |
|
399 | @HasRepoPermissionLevelDecorator('write') | |
@@ -425,11 +425,11 b' class FilesController(BaseRepoController' | |||||
425 | content = content.file |
|
425 | content = content.file | |
426 |
|
426 | |||
427 | if not content: |
|
427 | if not content: | |
428 |
|
|
428 | webutils.flash(_('No content'), category='warning') | |
429 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, |
|
429 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, | |
430 | revision='tip')) |
|
430 | revision='tip')) | |
431 | if not filename: |
|
431 | if not filename: | |
432 |
|
|
432 | webutils.flash(_('No filename'), category='warning') | |
433 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, |
|
433 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, | |
434 | revision='tip')) |
|
434 | revision='tip')) | |
435 | # strip all crap out of file, just leave the basename |
|
435 | # strip all crap out of file, just leave the basename | |
@@ -453,22 +453,22 b' class FilesController(BaseRepoController' | |||||
453 | author=author, |
|
453 | author=author, | |
454 | ) |
|
454 | ) | |
455 |
|
455 | |||
456 |
|
|
456 | webutils.flash(_('Successfully committed to %s') % node_path, | |
457 | category='success') |
|
457 | category='success') | |
458 | except NonRelativePathError as e: |
|
458 | except NonRelativePathError as e: | |
459 |
|
|
459 | webutils.flash(_('Location must be relative path and must not ' | |
460 | 'contain .. in path'), category='warning') |
|
460 | 'contain .. in path'), category='warning') | |
461 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, |
|
461 | raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name, | |
462 | revision='tip')) |
|
462 | revision='tip')) | |
463 | except (NodeError, NodeAlreadyExistsError) as e: |
|
463 | except (NodeError, NodeAlreadyExistsError) as e: | |
464 |
|
|
464 | webutils.flash(_(e), category='error') | |
465 | except Exception: |
|
465 | except Exception: | |
466 | log.error(traceback.format_exc()) |
|
466 | log.error(traceback.format_exc()) | |
467 |
|
|
467 | webutils.flash(_('Error occurred during commit'), category='error') | |
468 | raise HTTPFound(location=url('changeset_home', |
|
468 | raise HTTPFound(location=url('changeset_home', | |
469 | repo_name=c.repo_name, revision='tip')) |
|
469 | repo_name=c.repo_name, revision='tip')) | |
470 |
|
470 | |||
471 | return render('files/files_add.html') |
|
471 | return base.render('files/files_add.html') | |
472 |
|
472 | |||
473 | @LoginRequired(allow_default_user=True) |
|
473 | @LoginRequired(allow_default_user=True) | |
474 | @HasRepoPermissionLevelDecorator('read') |
|
474 | @HasRepoPermissionLevelDecorator('read') | |
@@ -505,13 +505,12 b' class FilesController(BaseRepoController' | |||||
505 | except (ImproperArchiveTypeError, KeyError): |
|
505 | except (ImproperArchiveTypeError, KeyError): | |
506 | return _('Unknown archive type') |
|
506 | return _('Unknown archive type') | |
507 |
|
507 | |||
508 | from kallithea import CONFIG |
|
|||
509 | rev_name = cs.raw_id[:12] |
|
508 | rev_name = cs.raw_id[:12] | |
510 | archive_name = '%s-%s%s' % (repo_name.replace('/', '_'), rev_name, ext) |
|
509 | archive_name = '%s-%s%s' % (repo_name.replace('/', '_'), rev_name, ext) | |
511 |
|
510 | |||
512 | archive_path = None |
|
511 | archive_path = None | |
513 | cached_archive_path = None |
|
512 | cached_archive_path = None | |
514 | archive_cache_dir = CONFIG.get('archive_cache_dir') |
|
513 | archive_cache_dir = kallithea.CONFIG.get('archive_cache_dir') | |
515 | if archive_cache_dir and not subrepos: # TODO: subrepo caching? |
|
514 | if archive_cache_dir and not subrepos: # TODO: subrepo caching? | |
516 | if not os.path.isdir(archive_cache_dir): |
|
515 | if not os.path.isdir(archive_cache_dir): | |
517 | os.makedirs(archive_cache_dir) |
|
516 | os.makedirs(archive_cache_dir) | |
@@ -547,7 +546,7 b' class FilesController(BaseRepoController' | |||||
547 | log.debug('Destroying temp archive %s', archive_path) |
|
546 | log.debug('Destroying temp archive %s', archive_path) | |
548 | os.remove(archive_path) |
|
547 | os.remove(archive_path) | |
549 |
|
548 | |||
550 | action_logger(user=request.authuser, |
|
549 | userlog.action_logger(user=request.authuser, | |
551 | action='user_downloaded_archive:%s' % (archive_name), |
|
550 | action='user_downloaded_archive:%s' % (archive_name), | |
552 | repo=repo_name, ipaddr=request.ip_addr, commit=True) |
|
551 | repo=repo_name, ipaddr=request.ip_addr, commit=True) | |
553 |
|
552 | |||
@@ -558,8 +557,8 b' class FilesController(BaseRepoController' | |||||
558 | @LoginRequired(allow_default_user=True) |
|
557 | @LoginRequired(allow_default_user=True) | |
559 | @HasRepoPermissionLevelDecorator('read') |
|
558 | @HasRepoPermissionLevelDecorator('read') | |
560 | def diff(self, repo_name, f_path): |
|
559 | def diff(self, repo_name, f_path): | |
561 | ignore_whitespace = request.GET.get('ignorews') == '1' |
|
560 | ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET) | |
562 | line_context = safe_int(request.GET.get('context'), 3) |
|
561 | diff_context_size = h.get_diff_context_size(request.GET) | |
563 | diff2 = request.GET.get('diff2', '') |
|
562 | diff2 = request.GET.get('diff2', '') | |
564 | diff1 = request.GET.get('diff1', '') or diff2 |
|
563 | diff1 = request.GET.get('diff1', '') or diff2 | |
565 | c.action = request.GET.get('diff') |
|
564 | c.action = request.GET.get('diff') | |
@@ -567,9 +566,6 b' class FilesController(BaseRepoController' | |||||
567 | c.f_path = f_path |
|
566 | c.f_path = f_path | |
568 | c.big_diff = False |
|
567 | c.big_diff = False | |
569 | fulldiff = request.GET.get('fulldiff') |
|
568 | fulldiff = request.GET.get('fulldiff') | |
570 | c.anchor_url = anchor_url |
|
|||
571 | c.ignorews_url = _ignorews_url |
|
|||
572 | c.context_url = _context_url |
|
|||
573 | c.changes = OrderedDict() |
|
569 | c.changes = OrderedDict() | |
574 | c.changes[diff2] = [] |
|
570 | c.changes[diff2] = [] | |
575 |
|
571 | |||
@@ -577,7 +573,7 b' class FilesController(BaseRepoController' | |||||
577 | # to reduce JS and callbacks |
|
573 | # to reduce JS and callbacks | |
578 |
|
574 | |||
579 | if request.GET.get('show_rev'): |
|
575 | if request.GET.get('show_rev'): | |
580 |
if s |
|
576 | if asbool(request.GET.get('annotate', 'False')): | |
581 | _url = url('files_annotate_home', repo_name=c.repo_name, |
|
577 | _url = url('files_annotate_home', repo_name=c.repo_name, | |
582 | revision=diff1, f_path=c.f_path) |
|
578 | revision=diff1, f_path=c.f_path) | |
583 | else: |
|
579 | else: | |
@@ -624,8 +620,8 b' class FilesController(BaseRepoController' | |||||
624 |
|
620 | |||
625 | if c.action == 'download': |
|
621 | if c.action == 'download': | |
626 | raw_diff = diffs.get_gitdiff(node1, node2, |
|
622 | raw_diff = diffs.get_gitdiff(node1, node2, | |
627 | ignore_whitespace=ignore_whitespace, |
|
623 | ignore_whitespace=ignore_whitespace_diff, | |
628 |
context= |
|
624 | context=diff_context_size) | |
629 | diff_name = '%s_vs_%s.diff' % (diff1, diff2) |
|
625 | diff_name = '%s_vs_%s.diff' % (diff1, diff2) | |
630 | response.content_type = 'text/plain' |
|
626 | response.content_type = 'text/plain' | |
631 | response.content_disposition = ( |
|
627 | response.content_disposition = ( | |
@@ -635,26 +631,21 b' class FilesController(BaseRepoController' | |||||
635 |
|
631 | |||
636 | elif c.action == 'raw': |
|
632 | elif c.action == 'raw': | |
637 | raw_diff = diffs.get_gitdiff(node1, node2, |
|
633 | raw_diff = diffs.get_gitdiff(node1, node2, | |
638 | ignore_whitespace=ignore_whitespace, |
|
634 | ignore_whitespace=ignore_whitespace_diff, | |
639 |
context= |
|
635 | context=diff_context_size) | |
640 | response.content_type = 'text/plain' |
|
636 | response.content_type = 'text/plain' | |
641 | return raw_diff |
|
637 | return raw_diff | |
642 |
|
638 | |||
643 | else: |
|
639 | else: | |
644 | fid = h.FID(diff2, node2.path) |
|
640 | fid = h.FID(diff2, node2.path) | |
645 | line_context_lcl = get_line_ctx(fid, request.GET) |
|
|||
646 | ign_whitespace_lcl = get_ignore_ws(fid, request.GET) |
|
|||
647 |
|
||||
648 | diff_limit = None if fulldiff else self.cut_off_limit |
|
641 | diff_limit = None if fulldiff else self.cut_off_limit | |
649 |
c.a_rev, c.cs_rev, a_path, diff, st, op = diffs. |
|
642 | c.a_rev, c.cs_rev, a_path, diff, st, op = diffs.html_diff(filenode_old=node1, | |
650 | filenode_new=node2, |
|
643 | filenode_new=node2, | |
651 | diff_limit=diff_limit, |
|
644 | diff_limit=diff_limit, | |
652 |
ignore_whitespace=ign_whitespace_ |
|
645 | ignore_whitespace=ignore_whitespace_diff, | |
653 |
line_context= |
|
646 | line_context=diff_context_size) | |
654 | enable_comments=False) |
|
|||
655 | c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)] |
|
647 | c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)] | |
656 |
|
648 | return base.render('files/file_diff.html') | ||
657 | return render('files/file_diff.html') |
|
|||
658 |
|
649 | |||
659 | @LoginRequired(allow_default_user=True) |
|
650 | @LoginRequired(allow_default_user=True) | |
660 | @HasRepoPermissionLevelDecorator('read') |
|
651 | @HasRepoPermissionLevelDecorator('read') | |
@@ -695,14 +686,14 b' class FilesController(BaseRepoController' | |||||
695 | node2 = FileNode(f_path, '', changeset=c.changeset_2) |
|
686 | node2 = FileNode(f_path, '', changeset=c.changeset_2) | |
696 | except ChangesetDoesNotExistError as e: |
|
687 | except ChangesetDoesNotExistError as e: | |
697 | msg = _('Such revision does not exist for this repository') |
|
688 | msg = _('Such revision does not exist for this repository') | |
698 |
|
|
689 | webutils.flash(msg, category='error') | |
699 | raise HTTPNotFound() |
|
690 | raise HTTPNotFound() | |
700 | c.node1 = node1 |
|
691 | c.node1 = node1 | |
701 | c.node2 = node2 |
|
692 | c.node2 = node2 | |
702 | c.cs1 = c.changeset_1 |
|
693 | c.cs1 = c.changeset_1 | |
703 | c.cs2 = c.changeset_2 |
|
694 | c.cs2 = c.changeset_2 | |
704 |
|
695 | |||
705 | return render('files/diff_2way.html') |
|
696 | return base.render('files/diff_2way.html') | |
706 |
|
697 | |||
707 | def _get_node_history(self, cs, f_path, changesets=None): |
|
698 | def _get_node_history(self, cs, f_path, changesets=None): | |
708 | """ |
|
699 | """ | |
@@ -745,7 +736,7 b' class FilesController(BaseRepoController' | |||||
745 |
|
736 | |||
746 | @LoginRequired(allow_default_user=True) |
|
737 | @LoginRequired(allow_default_user=True) | |
747 | @HasRepoPermissionLevelDecorator('read') |
|
738 | @HasRepoPermissionLevelDecorator('read') | |
748 | @jsonify |
|
739 | @base.jsonify | |
749 | def nodelist(self, repo_name, revision, f_path): |
|
740 | def nodelist(self, repo_name, revision, f_path): | |
750 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
741 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
751 | cs = self.__get_cs(revision) |
|
742 | cs = self.__get_cs(revision) |
@@ -30,28 +30,28 b' import logging' | |||||
30 | from tg import request |
|
30 | from tg import request | |
31 | from tg import tmpl_context as c |
|
31 | from tg import tmpl_context as c | |
32 |
|
32 | |||
|
33 | from kallithea.controllers import base | |||
33 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
34 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
34 | from kallithea.lib.base import BaseRepoController, render |
|
|||
35 | from kallithea.lib.page import Page |
|
35 | from kallithea.lib.page import Page | |
36 | from kallithea.lib.utils2 import safe_int |
|
36 | from kallithea.lib.utils2 import safe_int | |
37 |
from kallithea.model |
|
37 | from kallithea.model import db | |
38 |
|
38 | |||
39 |
|
39 | |||
40 | log = logging.getLogger(__name__) |
|
40 | log = logging.getLogger(__name__) | |
41 |
|
41 | |||
42 |
|
42 | |||
43 | class FollowersController(BaseRepoController): |
|
43 | class FollowersController(base.BaseRepoController): | |
44 |
|
44 | |||
45 | @LoginRequired(allow_default_user=True) |
|
45 | @LoginRequired(allow_default_user=True) | |
46 | @HasRepoPermissionLevelDecorator('read') |
|
46 | @HasRepoPermissionLevelDecorator('read') | |
47 | def followers(self, repo_name): |
|
47 | def followers(self, repo_name): | |
48 | p = safe_int(request.GET.get('page'), 1) |
|
48 | p = safe_int(request.GET.get('page'), 1) | |
49 | repo_id = c.db_repo.repo_id |
|
49 | repo_id = c.db_repo.repo_id | |
50 | d = UserFollowing.get_repo_followers(repo_id) \ |
|
50 | d = db.UserFollowing.get_repo_followers(repo_id) \ | |
51 | .order_by(UserFollowing.follows_from) |
|
51 | .order_by(db.UserFollowing.follows_from) | |
52 | c.followers_pager = Page(d, page=p, items_per_page=20) |
|
52 | c.followers_pager = Page(d, page=p, items_per_page=20) | |
53 |
|
53 | |||
54 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
54 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
55 | return render('/followers/followers_data.html') |
|
55 | return base.render('/followers/followers_data.html') | |
56 |
|
56 | |||
57 | return render('/followers/followers.html') |
|
57 | return base.render('/followers/followers.html') |
@@ -33,16 +33,15 b' from formencode import htmlfill' | |||||
33 | from tg import request |
|
33 | from tg import request | |
34 | from tg import tmpl_context as c |
|
34 | from tg import tmpl_context as c | |
35 | from tg.i18n import ugettext as _ |
|
35 | from tg.i18n import ugettext as _ | |
36 | from webob.exc import HTTPFound |
|
36 | from webob.exc import HTTPFound, HTTPNotFound | |
37 |
|
37 | |||
38 | import kallithea |
|
38 | import kallithea | |
39 | import kallithea.lib.helpers as h |
|
39 | from kallithea.controllers import base | |
40 |
from kallithea. |
|
40 | from kallithea.lib import webutils | |
41 |
from kallithea.lib.auth import |
|
41 | from kallithea.lib.auth import HasPermissionAnyDecorator, HasRepoPermissionLevel, HasRepoPermissionLevelDecorator, LoginRequired | |
42 | from kallithea.lib.base import BaseRepoController, render |
|
|||
43 | from kallithea.lib.page import Page |
|
42 | from kallithea.lib.page import Page | |
44 | from kallithea.lib.utils2 import safe_int |
|
43 | from kallithea.lib.utils2 import safe_int | |
45 |
from kallithea.model |
|
44 | from kallithea.model import db | |
46 | from kallithea.model.forms import RepoForkForm |
|
45 | from kallithea.model.forms import RepoForkForm | |
47 | from kallithea.model.repo import RepoModel |
|
46 | from kallithea.model.repo import RepoModel | |
48 | from kallithea.model.scm import AvailableRepoGroupChoices, ScmModel |
|
47 | from kallithea.model.scm import AvailableRepoGroupChoices, ScmModel | |
@@ -51,18 +50,14 b' from kallithea.model.scm import Availabl' | |||||
51 | log = logging.getLogger(__name__) |
|
50 | log = logging.getLogger(__name__) | |
52 |
|
51 | |||
53 |
|
52 | |||
54 | class ForksController(BaseRepoController): |
|
53 | class ForksController(base.BaseRepoController): | |
55 |
|
54 | |||
56 | def __load_defaults(self): |
|
55 | def __load_defaults(self): | |
57 | if HasPermissionAny('hg.create.write_on_repogroup.true')(): |
|
56 | c.repo_groups = AvailableRepoGroupChoices('write') | |
58 | repo_group_perm_level = 'write' |
|
|||
59 | else: |
|
|||
60 | repo_group_perm_level = 'admin' |
|
|||
61 | c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perm_level) |
|
|||
62 |
|
57 | |||
63 | c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs() |
|
58 | c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs() | |
64 |
|
59 | |||
65 | c.can_update = Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active |
|
60 | c.can_update = db.Ui.get_by_key('hooks', db.Ui.HOOK_UPDATE).ui_active | |
66 |
|
61 | |||
67 | def __load_data(self): |
|
62 | def __load_data(self): | |
68 | """ |
|
63 | """ | |
@@ -74,13 +69,12 b' class ForksController(BaseRepoController' | |||||
74 | repo = c.db_repo.scm_instance |
|
69 | repo = c.db_repo.scm_instance | |
75 |
|
70 | |||
76 | if c.repo_info is None: |
|
71 | if c.repo_info is None: | |
77 | h.not_mapped_error(c.repo_name) |
|
72 | raise HTTPNotFound() | |
78 | raise HTTPFound(location=url('repos')) |
|
|||
79 |
|
73 | |||
80 | c.default_user_id = kallithea.DEFAULT_USER_ID |
|
74 | c.default_user_id = kallithea.DEFAULT_USER_ID | |
81 | c.in_public_journal = UserFollowing.query() \ |
|
75 | c.in_public_journal = db.UserFollowing.query() \ | |
82 | .filter(UserFollowing.user_id == c.default_user_id) \ |
|
76 | .filter(db.UserFollowing.user_id == c.default_user_id) \ | |
83 | .filter(UserFollowing.follows_repository == c.repo_info).scalar() |
|
77 | .filter(db.UserFollowing.follows_repository == c.repo_info).scalar() | |
84 |
|
78 | |||
85 | if c.repo_info.stats: |
|
79 | if c.repo_info.stats: | |
86 | last_rev = c.repo_info.stats.stat_on_revision + 1 |
|
80 | last_rev = c.repo_info.stats.stat_on_revision + 1 | |
@@ -112,30 +106,29 b' class ForksController(BaseRepoController' | |||||
112 | p = safe_int(request.GET.get('page'), 1) |
|
106 | p = safe_int(request.GET.get('page'), 1) | |
113 | repo_id = c.db_repo.repo_id |
|
107 | repo_id = c.db_repo.repo_id | |
114 | d = [] |
|
108 | d = [] | |
115 | for r in Repository.get_repo_forks(repo_id): |
|
109 | for r in db.Repository.get_repo_forks(repo_id): | |
116 | if not HasRepoPermissionLevel('read')(r.repo_name, 'get forks check'): |
|
110 | if not HasRepoPermissionLevel('read')(r.repo_name, 'get forks check'): | |
117 | continue |
|
111 | continue | |
118 | d.append(r) |
|
112 | d.append(r) | |
119 | c.forks_pager = Page(d, page=p, items_per_page=20) |
|
113 | c.forks_pager = Page(d, page=p, items_per_page=20) | |
120 |
|
114 | |||
121 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
115 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
122 | return render('/forks/forks_data.html') |
|
116 | return base.render('/forks/forks_data.html') | |
123 |
|
117 | |||
124 | return render('/forks/forks.html') |
|
118 | return base.render('/forks/forks.html') | |
125 |
|
119 | |||
126 | @LoginRequired() |
|
120 | @LoginRequired() | |
127 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') |
|
121 | @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') | |
128 | @HasRepoPermissionLevelDecorator('read') |
|
122 | @HasRepoPermissionLevelDecorator('read') | |
129 | def fork(self, repo_name): |
|
123 | def fork(self, repo_name): | |
130 | c.repo_info = Repository.get_by_repo_name(repo_name) |
|
124 | c.repo_info = db.Repository.get_by_repo_name(repo_name) | |
131 | if not c.repo_info: |
|
125 | if not c.repo_info: | |
132 | h.not_mapped_error(repo_name) |
|
126 | raise HTTPNotFound() | |
133 | raise HTTPFound(location=url('home')) |
|
|||
134 |
|
127 | |||
135 | defaults = self.__load_data() |
|
128 | defaults = self.__load_data() | |
136 |
|
129 | |||
137 | return htmlfill.render( |
|
130 | return htmlfill.render( | |
138 | render('forks/fork.html'), |
|
131 | base.render('forks/fork.html'), | |
139 | defaults=defaults, |
|
132 | defaults=defaults, | |
140 | encoding="UTF-8", |
|
133 | encoding="UTF-8", | |
141 | force_defaults=False) |
|
134 | force_defaults=False) | |
@@ -145,26 +138,24 b' class ForksController(BaseRepoController' | |||||
145 | @HasRepoPermissionLevelDecorator('read') |
|
138 | @HasRepoPermissionLevelDecorator('read') | |
146 | def fork_create(self, repo_name): |
|
139 | def fork_create(self, repo_name): | |
147 | self.__load_defaults() |
|
140 | self.__load_defaults() | |
148 | c.repo_info = Repository.get_by_repo_name(repo_name) |
|
141 | c.repo_info = db.Repository.get_by_repo_name(repo_name) | |
149 | _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type}, |
|
142 | _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type}, | |
150 | repo_groups=c.repo_groups, |
|
143 | repo_groups=c.repo_groups, | |
151 | landing_revs=c.landing_revs_choices)() |
|
144 | landing_revs=c.landing_revs_choices)() | |
152 | form_result = {} |
|
145 | form_result = {} | |
153 | task_id = None |
|
|||
154 | try: |
|
146 | try: | |
155 | form_result = _form.to_python(dict(request.POST)) |
|
147 | form_result = _form.to_python(dict(request.POST)) | |
156 |
|
148 | |||
157 | # an approximation that is better than nothing |
|
149 | # an approximation that is better than nothing | |
158 | if not Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active: |
|
150 | if not db.Ui.get_by_key('hooks', db.Ui.HOOK_UPDATE).ui_active: | |
159 | form_result['update_after_clone'] = False |
|
151 | form_result['update_after_clone'] = False | |
160 |
|
152 | |||
161 | # create fork is done sometimes async on celery, db transaction |
|
153 | # create fork is done sometimes async on celery, db transaction | |
162 | # management is handled there. |
|
154 | # management is handled there. | |
163 |
|
|
155 | RepoModel().create_fork(form_result, request.authuser.user_id) | |
164 | task_id = task.task_id |
|
|||
165 | except formencode.Invalid as errors: |
|
156 | except formencode.Invalid as errors: | |
166 | return htmlfill.render( |
|
157 | return htmlfill.render( | |
167 | render('forks/fork.html'), |
|
158 | base.render('forks/fork.html'), | |
168 | defaults=errors.value, |
|
159 | defaults=errors.value, | |
169 | errors=errors.error_dict or {}, |
|
160 | errors=errors.error_dict or {}, | |
170 | prefix_error=False, |
|
161 | prefix_error=False, | |
@@ -172,9 +163,9 b' class ForksController(BaseRepoController' | |||||
172 | force_defaults=False) |
|
163 | force_defaults=False) | |
173 | except Exception: |
|
164 | except Exception: | |
174 | log.error(traceback.format_exc()) |
|
165 | log.error(traceback.format_exc()) | |
175 |
|
|
166 | webutils.flash(_('An error occurred during repository forking %s') % | |
176 | repo_name, category='error') |
|
167 | repo_name, category='error') | |
177 |
|
168 | |||
178 |
raise HTTPFound(location= |
|
169 | raise HTTPFound(location=webutils.url('repo_creating_home', | |
179 | repo_name=form_result['repo_name_full'], |
|
170 | repo_name=form_result['repo_name_full'], | |
180 |
|
|
171 | )) |
@@ -34,11 +34,11 b' from tg import tmpl_context as c' | |||||
34 | from tg.i18n import ugettext as _ |
|
34 | from tg.i18n import ugettext as _ | |
35 | from webob.exc import HTTPBadRequest |
|
35 | from webob.exc import HTTPBadRequest | |
36 |
|
36 | |||
37 |
|
|
37 | import kallithea.lib.helpers as h | |
|
38 | from kallithea.controllers import base | |||
38 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
39 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
39 | from kallithea.lib.base import BaseController, jsonify, render |
|
|||
40 | from kallithea.lib.utils2 import safe_str |
|
40 | from kallithea.lib.utils2 import safe_str | |
41 | from kallithea.model.db import RepoGroup, Repository, User, UserGroup |
|
41 | from kallithea.model import db | |
42 | from kallithea.model.repo import RepoModel |
|
42 | from kallithea.model.repo import RepoModel | |
43 | from kallithea.model.scm import UserGroupList |
|
43 | from kallithea.model.scm import UserGroupList | |
44 |
|
44 | |||
@@ -46,31 +46,31 b' from kallithea.model.scm import UserGrou' | |||||
46 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
47 |
|
47 | |||
48 |
|
48 | |||
49 | class HomeController(BaseController): |
|
49 | class HomeController(base.BaseController): | |
50 |
|
50 | |||
51 | def about(self): |
|
51 | def about(self): | |
52 | return render('/about.html') |
|
52 | return base.render('/about.html') | |
53 |
|
53 | |||
54 | @LoginRequired(allow_default_user=True) |
|
54 | @LoginRequired(allow_default_user=True) | |
55 | def index(self): |
|
55 | def index(self): | |
56 | c.group = None |
|
56 | c.group = None | |
57 |
|
57 | |||
58 | repo_groups_list = self.scm_model.get_repo_groups() |
|
58 | repo_groups_list = self.scm_model.get_repo_groups() | |
59 | repos_list = Repository.query(sorted=True).filter_by(group=None).all() |
|
59 | repos_list = db.Repository.query(sorted=True).filter_by(group=None).all() | |
60 |
|
60 | |||
61 | c.data = RepoModel().get_repos_as_dict(repos_list, |
|
61 | c.data = RepoModel().get_repos_as_dict(repos_list, | |
62 | repo_groups_list=repo_groups_list, |
|
62 | repo_groups_list=repo_groups_list, | |
63 | short_name=True) |
|
63 | short_name=True) | |
64 |
|
64 | |||
65 | return render('/index.html') |
|
65 | return base.render('/index.html') | |
66 |
|
66 | |||
67 | @LoginRequired(allow_default_user=True) |
|
67 | @LoginRequired(allow_default_user=True) | |
68 | @jsonify |
|
68 | @base.jsonify | |
69 | def repo_switcher_data(self): |
|
69 | def repo_switcher_data(self): | |
70 | if request.is_xhr: |
|
70 | if request.is_xhr: | |
71 | all_repos = Repository.query(sorted=True).all() |
|
71 | all_repos = db.Repository.query(sorted=True).all() | |
72 | repo_iter = self.scm_model.get_repos(all_repos) |
|
72 | repo_iter = self.scm_model.get_repos(all_repos) | |
73 | all_groups = RepoGroup.query(sorted=True).all() |
|
73 | all_groups = db.RepoGroup.query(sorted=True).all() | |
74 | repo_groups_iter = self.scm_model.get_repo_groups(all_groups) |
|
74 | repo_groups_iter = self.scm_model.get_repo_groups(all_groups) | |
75 |
|
75 | |||
76 | res = [{ |
|
76 | res = [{ | |
@@ -109,9 +109,9 b' class HomeController(BaseController):' | |||||
109 |
|
109 | |||
110 | @LoginRequired(allow_default_user=True) |
|
110 | @LoginRequired(allow_default_user=True) | |
111 | @HasRepoPermissionLevelDecorator('read') |
|
111 | @HasRepoPermissionLevelDecorator('read') | |
112 | @jsonify |
|
112 | @base.jsonify | |
113 | def repo_refs_data(self, repo_name): |
|
113 | def repo_refs_data(self, repo_name): | |
114 | repo = Repository.get_by_repo_name(repo_name).scm_instance |
|
114 | repo = db.Repository.get_by_repo_name(repo_name).scm_instance | |
115 | res = [] |
|
115 | res = [] | |
116 | _branches = repo.branches.items() |
|
116 | _branches = repo.branches.items() | |
117 | if _branches: |
|
117 | if _branches: | |
@@ -144,7 +144,7 b' class HomeController(BaseController):' | |||||
144 | return data |
|
144 | return data | |
145 |
|
145 | |||
146 | @LoginRequired() |
|
146 | @LoginRequired() | |
147 | @jsonify |
|
147 | @base.jsonify | |
148 | def users_and_groups_data(self): |
|
148 | def users_and_groups_data(self): | |
149 | """ |
|
149 | """ | |
150 | Returns 'results' with a list of users and user groups. |
|
150 | Returns 'results' with a list of users and user groups. | |
@@ -163,19 +163,20 b' class HomeController(BaseController):' | |||||
163 | if 'users' in types: |
|
163 | if 'users' in types: | |
164 | user_list = [] |
|
164 | user_list = [] | |
165 | if key: |
|
165 | if key: | |
166 | u = User.get_by_username(key) |
|
166 | u = db.User.get_by_username(key) | |
167 | if u: |
|
167 | if u: | |
168 | user_list = [u] |
|
168 | user_list = [u] | |
169 | elif query: |
|
169 | elif query: | |
170 | user_list = User.query() \ |
|
170 | user_list = db.User.query() \ | |
171 | .filter(User.is_default_user == False) \ |
|
171 | .filter(db.User.is_default_user == False) \ | |
172 | .filter(User.active == True) \ |
|
172 | .filter(db.User.active == True) \ | |
173 | .filter(or_( |
|
173 | .filter(or_( | |
174 | User.username.ilike("%%" + query + "%%"), |
|
174 | db.User.username.ilike("%%" + query + "%%"), | |
175 | User.name.ilike("%%" + query + "%%"), |
|
175 | db.User.name.concat(' ').concat(db.User.lastname).ilike("%%" + query + "%%"), | |
176 | User.lastname.ilike("%%" + query + "%%"), |
|
176 | db.User.lastname.concat(' ').concat(db.User.name).ilike("%%" + query + "%%"), | |
|
177 | db.User.email.ilike("%%" + query + "%%"), | |||
177 | )) \ |
|
178 | )) \ | |
178 | .order_by(User.username) \ |
|
179 | .order_by(db.User.username) \ | |
179 | .limit(500) \ |
|
180 | .limit(500) \ | |
180 | .all() |
|
181 | .all() | |
181 | for u in user_list: |
|
182 | for u in user_list: | |
@@ -191,14 +192,14 b' class HomeController(BaseController):' | |||||
191 | if 'groups' in types: |
|
192 | if 'groups' in types: | |
192 | grp_list = [] |
|
193 | grp_list = [] | |
193 | if key: |
|
194 | if key: | |
194 | grp = UserGroup.get_by_group_name(key) |
|
195 | grp = db.UserGroup.get_by_group_name(key) | |
195 | if grp: |
|
196 | if grp: | |
196 | grp_list = [grp] |
|
197 | grp_list = [grp] | |
197 | elif query: |
|
198 | elif query: | |
198 | grp_list = UserGroup.query() \ |
|
199 | grp_list = db.UserGroup.query() \ | |
199 | .filter(UserGroup.users_group_name.ilike("%%" + query + "%%")) \ |
|
200 | .filter(db.UserGroup.users_group_name.ilike("%%" + query + "%%")) \ | |
200 | .filter(UserGroup.users_group_active == True) \ |
|
201 | .filter(db.UserGroup.users_group_active == True) \ | |
201 | .order_by(UserGroup.users_group_name) \ |
|
202 | .order_by(db.UserGroup.users_group_name) \ | |
202 | .limit(500) \ |
|
203 | .limit(500) \ | |
203 | .all() |
|
204 | .all() | |
204 | for g in UserGroupList(grp_list, perm_level='read'): |
|
205 | for g in UserGroupList(grp_list, perm_level='read'): |
@@ -37,14 +37,13 b' from tg.i18n import ugettext as _' | |||||
37 | from webob.exc import HTTPBadRequest |
|
37 | from webob.exc import HTTPBadRequest | |
38 |
|
38 | |||
39 | import kallithea.lib.helpers as h |
|
39 | import kallithea.lib.helpers as h | |
|
40 | from kallithea.controllers import base | |||
40 | from kallithea.controllers.admin.admin import _journal_filter |
|
41 | from kallithea.controllers.admin.admin import _journal_filter | |
41 | from kallithea.lib import feeds |
|
42 | from kallithea.lib import feeds, webutils | |
42 | from kallithea.lib.auth import LoginRequired |
|
43 | from kallithea.lib.auth import LoginRequired | |
43 | from kallithea.lib.base import BaseController, render |
|
|||
44 | from kallithea.lib.page import Page |
|
44 | from kallithea.lib.page import Page | |
45 | from kallithea.lib.utils2 import AttributeDict, safe_int |
|
45 | from kallithea.lib.utils2 import AttributeDict, safe_int | |
46 | from kallithea.model.db import Repository, User, UserFollowing, UserLog |
|
46 | from kallithea.model import db, meta | |
47 | from kallithea.model.meta import Session |
|
|||
48 | from kallithea.model.repo import RepoModel |
|
47 | from kallithea.model.repo import RepoModel | |
49 |
|
48 | |||
50 |
|
49 | |||
@@ -56,7 +55,7 b' ttl = "5"' | |||||
56 | feed_nr = 20 |
|
55 | feed_nr = 20 | |
57 |
|
56 | |||
58 |
|
57 | |||
59 | class JournalController(BaseController): |
|
58 | class JournalController(base.BaseController): | |
60 |
|
59 | |||
61 | def _before(self, *args, **kwargs): |
|
60 | def _before(self, *args, **kwargs): | |
62 | super(JournalController, self)._before(*args, **kwargs) |
|
61 | super(JournalController, self)._before(*args, **kwargs) | |
@@ -84,20 +83,20 b' class JournalController(BaseController):' | |||||
84 | filtering_criterion = None |
|
83 | filtering_criterion = None | |
85 |
|
84 | |||
86 | if repo_ids and user_ids: |
|
85 | if repo_ids and user_ids: | |
87 | filtering_criterion = or_(UserLog.repository_id.in_(repo_ids), |
|
86 | filtering_criterion = or_(db.UserLog.repository_id.in_(repo_ids), | |
88 | UserLog.user_id.in_(user_ids)) |
|
87 | db.UserLog.user_id.in_(user_ids)) | |
89 | if repo_ids and not user_ids: |
|
88 | if repo_ids and not user_ids: | |
90 | filtering_criterion = UserLog.repository_id.in_(repo_ids) |
|
89 | filtering_criterion = db.UserLog.repository_id.in_(repo_ids) | |
91 | if not repo_ids and user_ids: |
|
90 | if not repo_ids and user_ids: | |
92 | filtering_criterion = UserLog.user_id.in_(user_ids) |
|
91 | filtering_criterion = db.UserLog.user_id.in_(user_ids) | |
93 | if filtering_criterion is not None: |
|
92 | if filtering_criterion is not None: | |
94 | journal = UserLog.query() \ |
|
93 | journal = db.UserLog.query() \ | |
95 | .options(joinedload(UserLog.user)) \ |
|
94 | .options(joinedload(db.UserLog.user)) \ | |
96 | .options(joinedload(UserLog.repository)) |
|
95 | .options(joinedload(db.UserLog.repository)) | |
97 | # filter |
|
96 | # filter | |
98 | journal = _journal_filter(journal, c.search_term) |
|
97 | journal = _journal_filter(journal, c.search_term) | |
99 | journal = journal.filter(filtering_criterion) \ |
|
98 | journal = journal.filter(filtering_criterion) \ | |
100 | .order_by(UserLog.action_date.desc()) |
|
99 | .order_by(db.UserLog.action_date.desc()) | |
101 | else: |
|
100 | else: | |
102 | journal = [] |
|
101 | journal = [] | |
103 |
|
102 | |||
@@ -126,13 +125,13 b' class JournalController(BaseController):' | |||||
126 | entry.repository.repo_name) |
|
125 | entry.repository.repo_name) | |
127 | _url = None |
|
126 | _url = None | |
128 | if entry.repository is not None: |
|
127 | if entry.repository is not None: | |
129 |
_url = |
|
128 | _url = webutils.canonical_url('changelog_home', | |
130 | repo_name=entry.repository.repo_name) |
|
129 | repo_name=entry.repository.repo_name) | |
131 |
|
130 | |||
132 | entries.append(dict( |
|
131 | entries.append(dict( | |
133 | title=title, |
|
132 | title=title, | |
134 | pubdate=entry.action_date, |
|
133 | pubdate=entry.action_date, | |
135 |
link=_url or |
|
134 | link=_url or webutils.canonical_url(''), | |
136 | author_email=user.email, |
|
135 | author_email=user.email, | |
137 | author_name=user.full_name_or_username, |
|
136 | author_name=user.full_name_or_username, | |
138 | description=action_extra(), |
|
137 | description=action_extra(), | |
@@ -142,22 +141,22 b' class JournalController(BaseController):' | |||||
142 |
|
141 | |||
143 | def _atom_feed(self, repos, public=True): |
|
142 | def _atom_feed(self, repos, public=True): | |
144 | if public: |
|
143 | if public: | |
145 |
link = |
|
144 | link = webutils.canonical_url('public_journal_atom') | |
146 | desc = '%s %s %s' % (c.site_name, _('Public Journal'), |
|
145 | desc = '%s %s %s' % (c.site_name, _('Public Journal'), | |
147 | 'atom feed') |
|
146 | 'atom feed') | |
148 | else: |
|
147 | else: | |
149 |
link = |
|
148 | link = webutils.canonical_url('journal_atom') | |
150 | desc = '%s %s %s' % (c.site_name, _('Journal'), 'atom feed') |
|
149 | desc = '%s %s %s' % (c.site_name, _('Journal'), 'atom feed') | |
151 |
|
150 | |||
152 | return self._feed(repos, feeds.AtomFeed, link, desc) |
|
151 | return self._feed(repos, feeds.AtomFeed, link, desc) | |
153 |
|
152 | |||
154 | def _rss_feed(self, repos, public=True): |
|
153 | def _rss_feed(self, repos, public=True): | |
155 | if public: |
|
154 | if public: | |
156 |
link = |
|
155 | link = webutils.canonical_url('public_journal_atom') | |
157 | desc = '%s %s %s' % (c.site_name, _('Public Journal'), |
|
156 | desc = '%s %s %s' % (c.site_name, _('Public Journal'), | |
158 | 'rss feed') |
|
157 | 'rss feed') | |
159 | else: |
|
158 | else: | |
160 |
link = |
|
159 | link = webutils.canonical_url('journal_atom') | |
161 | desc = '%s %s %s' % (c.site_name, _('Journal'), 'rss feed') |
|
160 | desc = '%s %s %s' % (c.site_name, _('Journal'), 'rss feed') | |
162 |
|
161 | |||
163 | return self._feed(repos, feeds.RssFeed, link, desc) |
|
162 | return self._feed(repos, feeds.RssFeed, link, desc) | |
@@ -166,10 +165,10 b' class JournalController(BaseController):' | |||||
166 | def index(self): |
|
165 | def index(self): | |
167 | # Return a rendered template |
|
166 | # Return a rendered template | |
168 | p = safe_int(request.GET.get('page'), 1) |
|
167 | p = safe_int(request.GET.get('page'), 1) | |
169 | c.user = User.get(request.authuser.user_id) |
|
168 | c.user = db.User.get(request.authuser.user_id) | |
170 | c.following = UserFollowing.query() \ |
|
169 | c.following = db.UserFollowing.query() \ | |
171 | .filter(UserFollowing.user_id == request.authuser.user_id) \ |
|
170 | .filter(db.UserFollowing.user_id == request.authuser.user_id) \ | |
172 | .options(joinedload(UserFollowing.follows_repository)) \ |
|
171 | .options(joinedload(db.UserFollowing.follows_repository)) \ | |
173 | .all() |
|
172 | .all() | |
174 |
|
173 | |||
175 | journal = self._get_journal_data(c.following) |
|
174 | journal = self._get_journal_data(c.following) | |
@@ -179,32 +178,32 b' class JournalController(BaseController):' | |||||
179 | c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager) |
|
178 | c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager) | |
180 |
|
179 | |||
181 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
180 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
182 | return render('journal/journal_data.html') |
|
181 | return base.render('journal/journal_data.html') | |
183 |
|
182 | |||
184 | repos_list = Repository.query(sorted=True) \ |
|
183 | repos_list = db.Repository.query(sorted=True) \ | |
185 | .filter_by(owner_id=request.authuser.user_id).all() |
|
184 | .filter_by(owner_id=request.authuser.user_id).all() | |
186 |
|
185 | |||
187 | repos_data = RepoModel().get_repos_as_dict(repos_list, admin=True) |
|
186 | repos_data = RepoModel().get_repos_as_dict(repos_list, admin=True) | |
188 | # data used to render the grid |
|
187 | # data used to render the grid | |
189 | c.data = repos_data |
|
188 | c.data = repos_data | |
190 |
|
189 | |||
191 | return render('journal/journal.html') |
|
190 | return base.render('journal/journal.html') | |
192 |
|
191 | |||
193 | @LoginRequired() |
|
192 | @LoginRequired() | |
194 | def journal_atom(self): |
|
193 | def journal_atom(self): | |
195 | """Produce a simple atom-1.0 feed""" |
|
194 | """Produce a simple atom-1.0 feed""" | |
196 | following = UserFollowing.query() \ |
|
195 | following = db.UserFollowing.query() \ | |
197 | .filter(UserFollowing.user_id == request.authuser.user_id) \ |
|
196 | .filter(db.UserFollowing.user_id == request.authuser.user_id) \ | |
198 | .options(joinedload(UserFollowing.follows_repository)) \ |
|
197 | .options(joinedload(db.UserFollowing.follows_repository)) \ | |
199 | .all() |
|
198 | .all() | |
200 | return self._atom_feed(following, public=False) |
|
199 | return self._atom_feed(following, public=False) | |
201 |
|
200 | |||
202 | @LoginRequired() |
|
201 | @LoginRequired() | |
203 | def journal_rss(self): |
|
202 | def journal_rss(self): | |
204 | """Produce a simple rss2 feed""" |
|
203 | """Produce a simple rss2 feed""" | |
205 | following = UserFollowing.query() \ |
|
204 | following = db.UserFollowing.query() \ | |
206 | .filter(UserFollowing.user_id == request.authuser.user_id) \ |
|
205 | .filter(db.UserFollowing.user_id == request.authuser.user_id) \ | |
207 | .options(joinedload(UserFollowing.follows_repository)) \ |
|
206 | .options(joinedload(db.UserFollowing.follows_repository)) \ | |
208 | .all() |
|
207 | .all() | |
209 | return self._rss_feed(following, public=False) |
|
208 | return self._rss_feed(following, public=False) | |
210 |
|
209 | |||
@@ -215,7 +214,7 b' class JournalController(BaseController):' | |||||
215 | try: |
|
214 | try: | |
216 | self.scm_model.toggle_following_user(user_id, |
|
215 | self.scm_model.toggle_following_user(user_id, | |
217 | request.authuser.user_id) |
|
216 | request.authuser.user_id) | |
218 | Session().commit() |
|
217 | meta.Session().commit() | |
219 | return 'ok' |
|
218 | return 'ok' | |
220 | except Exception: |
|
219 | except Exception: | |
221 | log.error(traceback.format_exc()) |
|
220 | log.error(traceback.format_exc()) | |
@@ -226,7 +225,7 b' class JournalController(BaseController):' | |||||
226 | try: |
|
225 | try: | |
227 | self.scm_model.toggle_following_repo(repo_id, |
|
226 | self.scm_model.toggle_following_repo(repo_id, | |
228 | request.authuser.user_id) |
|
227 | request.authuser.user_id) | |
229 | Session().commit() |
|
228 | meta.Session().commit() | |
230 | return 'ok' |
|
229 | return 'ok' | |
231 | except Exception: |
|
230 | except Exception: | |
232 | log.error(traceback.format_exc()) |
|
231 | log.error(traceback.format_exc()) | |
@@ -239,9 +238,9 b' class JournalController(BaseController):' | |||||
239 | # Return a rendered template |
|
238 | # Return a rendered template | |
240 | p = safe_int(request.GET.get('page'), 1) |
|
239 | p = safe_int(request.GET.get('page'), 1) | |
241 |
|
240 | |||
242 | c.following = UserFollowing.query() \ |
|
241 | c.following = db.UserFollowing.query() \ | |
243 | .filter(UserFollowing.user_id == request.authuser.user_id) \ |
|
242 | .filter(db.UserFollowing.user_id == request.authuser.user_id) \ | |
244 | .options(joinedload(UserFollowing.follows_repository)) \ |
|
243 | .options(joinedload(db.UserFollowing.follows_repository)) \ | |
245 | .all() |
|
244 | .all() | |
246 |
|
245 | |||
247 | journal = self._get_journal_data(c.following) |
|
246 | journal = self._get_journal_data(c.following) | |
@@ -251,16 +250,16 b' class JournalController(BaseController):' | |||||
251 | c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager) |
|
250 | c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager) | |
252 |
|
251 | |||
253 | if request.environ.get('HTTP_X_PARTIAL_XHR'): |
|
252 | if request.environ.get('HTTP_X_PARTIAL_XHR'): | |
254 | return render('journal/journal_data.html') |
|
253 | return base.render('journal/journal_data.html') | |
255 |
|
254 | |||
256 | return render('journal/public_journal.html') |
|
255 | return base.render('journal/public_journal.html') | |
257 |
|
256 | |||
258 | @LoginRequired(allow_default_user=True) |
|
257 | @LoginRequired(allow_default_user=True) | |
259 | def public_journal_atom(self): |
|
258 | def public_journal_atom(self): | |
260 | """Produce a simple atom-1.0 feed""" |
|
259 | """Produce a simple atom-1.0 feed""" | |
261 | c.following = UserFollowing.query() \ |
|
260 | c.following = db.UserFollowing.query() \ | |
262 | .filter(UserFollowing.user_id == request.authuser.user_id) \ |
|
261 | .filter(db.UserFollowing.user_id == request.authuser.user_id) \ | |
263 | .options(joinedload(UserFollowing.follows_repository)) \ |
|
262 | .options(joinedload(db.UserFollowing.follows_repository)) \ | |
264 | .all() |
|
263 | .all() | |
265 |
|
264 | |||
266 | return self._atom_feed(c.following) |
|
265 | return self._atom_feed(c.following) | |
@@ -268,9 +267,9 b' class JournalController(BaseController):' | |||||
268 | @LoginRequired(allow_default_user=True) |
|
267 | @LoginRequired(allow_default_user=True) | |
269 | def public_journal_rss(self): |
|
268 | def public_journal_rss(self): | |
270 | """Produce a simple rss2 feed""" |
|
269 | """Produce a simple rss2 feed""" | |
271 | c.following = UserFollowing.query() \ |
|
270 | c.following = db.UserFollowing.query() \ | |
272 | .filter(UserFollowing.user_id == request.authuser.user_id) \ |
|
271 | .filter(db.UserFollowing.user_id == request.authuser.user_id) \ | |
273 | .options(joinedload(UserFollowing.follows_repository)) \ |
|
272 | .options(joinedload(db.UserFollowing.follows_repository)) \ | |
274 | .all() |
|
273 | .all() | |
275 |
|
274 | |||
276 | return self._rss_feed(c.following) |
|
275 | return self._rss_feed(c.following) |
@@ -36,21 +36,21 b' from tg import tmpl_context as c' | |||||
36 | from tg.i18n import ugettext as _ |
|
36 | from tg.i18n import ugettext as _ | |
37 | from webob.exc import HTTPBadRequest, HTTPFound |
|
37 | from webob.exc import HTTPBadRequest, HTTPFound | |
38 |
|
38 | |||
39 | import kallithea.lib.helpers as h |
|
39 | from kallithea.controllers import base | |
40 |
from kallithea. |
|
40 | from kallithea.lib import webutils | |
41 | from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator |
|
41 | from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator | |
42 | from kallithea.lib.base import BaseController, log_in_user, render |
|
|||
43 | from kallithea.lib.exceptions import UserCreationError |
|
42 | from kallithea.lib.exceptions import UserCreationError | |
44 | from kallithea.model.db import Setting, User |
|
43 | from kallithea.lib.recaptcha import submit | |
|
44 | from kallithea.lib.webutils import url | |||
|
45 | from kallithea.model import db, meta | |||
45 | from kallithea.model.forms import LoginForm, PasswordResetConfirmationForm, PasswordResetRequestForm, RegisterForm |
|
46 | from kallithea.model.forms import LoginForm, PasswordResetConfirmationForm, PasswordResetRequestForm, RegisterForm | |
46 | from kallithea.model.meta import Session |
|
|||
47 | from kallithea.model.user import UserModel |
|
47 | from kallithea.model.user import UserModel | |
48 |
|
48 | |||
49 |
|
49 | |||
50 | log = logging.getLogger(__name__) |
|
50 | log = logging.getLogger(__name__) | |
51 |
|
51 | |||
52 |
|
52 | |||
53 | class LoginController(BaseController): |
|
53 | class LoginController(base.BaseController): | |
54 |
|
54 | |||
55 | def _validate_came_from(self, came_from, |
|
55 | def _validate_came_from(self, came_from, | |
56 | _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")): |
|
56 | _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")): | |
@@ -82,14 +82,14 b' class LoginController(BaseController):' | |||||
82 | # login_form will check username/password using ValidAuth and report failure to the user |
|
82 | # login_form will check username/password using ValidAuth and report failure to the user | |
83 | c.form_result = login_form.to_python(dict(request.POST)) |
|
83 | c.form_result = login_form.to_python(dict(request.POST)) | |
84 | username = c.form_result['username'] |
|
84 | username = c.form_result['username'] | |
85 | user = User.get_by_username_or_email(username) |
|
85 | user = db.User.get_by_username_or_email(username) | |
86 | assert user is not None # the same user get just passed in the form validation |
|
86 | assert user is not None # the same user get just passed in the form validation | |
87 | except formencode.Invalid as errors: |
|
87 | except formencode.Invalid as errors: | |
88 | defaults = errors.value |
|
88 | defaults = errors.value | |
89 | # remove password from filling in form again |
|
89 | # remove password from filling in form again | |
90 | defaults.pop('password', None) |
|
90 | defaults.pop('password', None) | |
91 | return htmlfill.render( |
|
91 | return htmlfill.render( | |
92 | render('/login.html'), |
|
92 | base.render('/login.html'), | |
93 | defaults=errors.value, |
|
93 | defaults=errors.value, | |
94 | errors=errors.error_dict or {}, |
|
94 | errors=errors.error_dict or {}, | |
95 | prefix_error=False, |
|
95 | prefix_error=False, | |
@@ -100,28 +100,28 b' class LoginController(BaseController):' | |||||
100 | # the fly can throw this exception signaling that there's issue |
|
100 | # the fly can throw this exception signaling that there's issue | |
101 | # with user creation, explanation should be provided in |
|
101 | # with user creation, explanation should be provided in | |
102 | # Exception itself |
|
102 | # Exception itself | |
103 |
|
|
103 | webutils.flash(e, 'error') | |
104 | else: |
|
104 | else: | |
105 | # login_form already validated the password - now set the session cookie accordingly |
|
105 | # login_form already validated the password - now set the session cookie accordingly | |
106 | auth_user = log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr) |
|
106 | auth_user = base.log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr) | |
107 | if auth_user: |
|
107 | if auth_user: | |
108 | raise HTTPFound(location=c.came_from) |
|
108 | raise HTTPFound(location=c.came_from) | |
109 |
|
|
109 | webutils.flash(_('Authentication failed.'), 'error') | |
110 | else: |
|
110 | else: | |
111 | # redirect if already logged in |
|
111 | # redirect if already logged in | |
112 | if not request.authuser.is_anonymous: |
|
112 | if not request.authuser.is_anonymous: | |
113 | raise HTTPFound(location=c.came_from) |
|
113 | raise HTTPFound(location=c.came_from) | |
114 | # continue to show login to default user |
|
114 | # continue to show login to default user | |
115 |
|
115 | |||
116 | return render('/login.html') |
|
116 | return base.render('/login.html') | |
117 |
|
117 | |||
118 | @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate', |
|
118 | @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate', | |
119 | 'hg.register.manual_activate') |
|
119 | 'hg.register.manual_activate') | |
120 | def register(self): |
|
120 | def register(self): | |
121 |
def_user_perms = AuthUser(dbuser=User.get_default_user()).permissions |
|
121 | def_user_perms = AuthUser(dbuser=db.User.get_default_user()).global_permissions | |
122 | c.auto_active = 'hg.register.auto_activate' in def_user_perms |
|
122 | c.auto_active = 'hg.register.auto_activate' in def_user_perms | |
123 |
|
123 | |||
124 | settings = Setting.get_app_settings() |
|
124 | settings = db.Setting.get_app_settings() | |
125 | captcha_private_key = settings.get('captcha_private_key') |
|
125 | captcha_private_key = settings.get('captcha_private_key') | |
126 | c.captcha_active = bool(captcha_private_key) |
|
126 | c.captcha_active = bool(captcha_private_key) | |
127 | c.captcha_public_key = settings.get('captcha_public_key') |
|
127 | c.captcha_public_key = settings.get('captcha_public_key') | |
@@ -133,7 +133,6 b' class LoginController(BaseController):' | |||||
133 | form_result['active'] = c.auto_active |
|
133 | form_result['active'] = c.auto_active | |
134 |
|
134 | |||
135 | if c.captcha_active: |
|
135 | if c.captcha_active: | |
136 | from kallithea.lib.recaptcha import submit |
|
|||
137 | response = submit(request.POST.get('g-recaptcha-response'), |
|
136 | response = submit(request.POST.get('g-recaptcha-response'), | |
138 | private_key=captcha_private_key, |
|
137 | private_key=captcha_private_key, | |
139 | remoteip=request.ip_addr) |
|
138 | remoteip=request.ip_addr) | |
@@ -145,14 +144,14 b' class LoginController(BaseController):' | |||||
145 | error_dict=error_dict) |
|
144 | error_dict=error_dict) | |
146 |
|
145 | |||
147 | UserModel().create_registration(form_result) |
|
146 | UserModel().create_registration(form_result) | |
148 |
|
|
147 | webutils.flash(_('You have successfully registered with %s') % (c.site_name or 'Kallithea'), | |
149 | category='success') |
|
148 | category='success') | |
150 | Session().commit() |
|
149 | meta.Session().commit() | |
151 | raise HTTPFound(location=url('login_home')) |
|
150 | raise HTTPFound(location=url('login_home')) | |
152 |
|
151 | |||
153 | except formencode.Invalid as errors: |
|
152 | except formencode.Invalid as errors: | |
154 | return htmlfill.render( |
|
153 | return htmlfill.render( | |
155 | render('/register.html'), |
|
154 | base.render('/register.html'), | |
156 | defaults=errors.value, |
|
155 | defaults=errors.value, | |
157 | errors=errors.error_dict or {}, |
|
156 | errors=errors.error_dict or {}, | |
158 | prefix_error=False, |
|
157 | prefix_error=False, | |
@@ -163,12 +162,12 b' class LoginController(BaseController):' | |||||
163 | # the fly can throw this exception signaling that there's issue |
|
162 | # the fly can throw this exception signaling that there's issue | |
164 | # with user creation, explanation should be provided in |
|
163 | # with user creation, explanation should be provided in | |
165 | # Exception itself |
|
164 | # Exception itself | |
166 |
|
|
165 | webutils.flash(e, 'error') | |
167 |
|
166 | |||
168 | return render('/register.html') |
|
167 | return base.render('/register.html') | |
169 |
|
168 | |||
170 | def password_reset(self): |
|
169 | def password_reset(self): | |
171 | settings = Setting.get_app_settings() |
|
170 | settings = db.Setting.get_app_settings() | |
172 | captcha_private_key = settings.get('captcha_private_key') |
|
171 | captcha_private_key = settings.get('captcha_private_key') | |
173 | c.captcha_active = bool(captcha_private_key) |
|
172 | c.captcha_active = bool(captcha_private_key) | |
174 | c.captcha_public_key = settings.get('captcha_public_key') |
|
173 | c.captcha_public_key = settings.get('captcha_public_key') | |
@@ -178,7 +177,6 b' class LoginController(BaseController):' | |||||
178 | try: |
|
177 | try: | |
179 | form_result = password_reset_form.to_python(dict(request.POST)) |
|
178 | form_result = password_reset_form.to_python(dict(request.POST)) | |
180 | if c.captcha_active: |
|
179 | if c.captcha_active: | |
181 | from kallithea.lib.recaptcha import submit |
|
|||
182 | response = submit(request.POST.get('g-recaptcha-response'), |
|
180 | response = submit(request.POST.get('g-recaptcha-response'), | |
183 | private_key=captcha_private_key, |
|
181 | private_key=captcha_private_key, | |
184 | remoteip=request.ip_addr) |
|
182 | remoteip=request.ip_addr) | |
@@ -189,20 +187,20 b' class LoginController(BaseController):' | |||||
189 | raise formencode.Invalid(_msg, _value, None, |
|
187 | raise formencode.Invalid(_msg, _value, None, | |
190 | error_dict=error_dict) |
|
188 | error_dict=error_dict) | |
191 | redirect_link = UserModel().send_reset_password_email(form_result) |
|
189 | redirect_link = UserModel().send_reset_password_email(form_result) | |
192 |
|
|
190 | webutils.flash(_('A password reset confirmation code has been sent'), | |
193 | category='success') |
|
191 | category='success') | |
194 | raise HTTPFound(location=redirect_link) |
|
192 | raise HTTPFound(location=redirect_link) | |
195 |
|
193 | |||
196 | except formencode.Invalid as errors: |
|
194 | except formencode.Invalid as errors: | |
197 | return htmlfill.render( |
|
195 | return htmlfill.render( | |
198 | render('/password_reset.html'), |
|
196 | base.render('/password_reset.html'), | |
199 | defaults=errors.value, |
|
197 | defaults=errors.value, | |
200 | errors=errors.error_dict or {}, |
|
198 | errors=errors.error_dict or {}, | |
201 | prefix_error=False, |
|
199 | prefix_error=False, | |
202 | encoding="UTF-8", |
|
200 | encoding="UTF-8", | |
203 | force_defaults=False) |
|
201 | force_defaults=False) | |
204 |
|
202 | |||
205 | return render('/password_reset.html') |
|
203 | return base.render('/password_reset.html') | |
206 |
|
204 | |||
207 | def password_reset_confirmation(self): |
|
205 | def password_reset_confirmation(self): | |
208 | # This controller handles both GET and POST requests, though we |
|
206 | # This controller handles both GET and POST requests, though we | |
@@ -215,14 +213,14 b' class LoginController(BaseController):' | |||||
215 | c.timestamp = request.params.get('timestamp') or '' |
|
213 | c.timestamp = request.params.get('timestamp') or '' | |
216 | c.token = request.params.get('token') or '' |
|
214 | c.token = request.params.get('token') or '' | |
217 | if not request.POST: |
|
215 | if not request.POST: | |
218 | return render('/password_reset_confirmation.html') |
|
216 | return base.render('/password_reset_confirmation.html') | |
219 |
|
217 | |||
220 | form = PasswordResetConfirmationForm()() |
|
218 | form = PasswordResetConfirmationForm()() | |
221 | try: |
|
219 | try: | |
222 | form_result = form.to_python(dict(request.POST)) |
|
220 | form_result = form.to_python(dict(request.POST)) | |
223 | except formencode.Invalid as errors: |
|
221 | except formencode.Invalid as errors: | |
224 | return htmlfill.render( |
|
222 | return htmlfill.render( | |
225 | render('/password_reset_confirmation.html'), |
|
223 | base.render('/password_reset_confirmation.html'), | |
226 | defaults=errors.value, |
|
224 | defaults=errors.value, | |
227 | errors=errors.error_dict or {}, |
|
225 | errors=errors.error_dict or {}, | |
228 | prefix_error=False, |
|
226 | prefix_error=False, | |
@@ -234,14 +232,14 b' class LoginController(BaseController):' | |||||
234 | form_result['token'], |
|
232 | form_result['token'], | |
235 | ): |
|
233 | ): | |
236 | return htmlfill.render( |
|
234 | return htmlfill.render( | |
237 | render('/password_reset_confirmation.html'), |
|
235 | base.render('/password_reset_confirmation.html'), | |
238 | defaults=form_result, |
|
236 | defaults=form_result, | |
239 | errors={'token': _('Invalid password reset token')}, |
|
237 | errors={'token': _('Invalid password reset token')}, | |
240 | prefix_error=False, |
|
238 | prefix_error=False, | |
241 | encoding='UTF-8') |
|
239 | encoding='UTF-8') | |
242 |
|
240 | |||
243 | UserModel().reset_password(form_result['email'], form_result['password']) |
|
241 | UserModel().reset_password(form_result['email'], form_result['password']) | |
244 |
|
|
242 | webutils.flash(_('Successfully updated password'), category='success') | |
245 | raise HTTPFound(location=url('login_home')) |
|
243 | raise HTTPFound(location=url('login_home')) | |
246 |
|
244 | |||
247 | def logout(self): |
|
245 | def logout(self): | |
@@ -255,4 +253,4 b' class LoginController(BaseController):' | |||||
255 | Only intended for testing but might also be useful for other kinds |
|
253 | Only intended for testing but might also be useful for other kinds | |
256 | of automation. |
|
254 | of automation. | |
257 | """ |
|
255 | """ | |
258 |
return |
|
256 | return webutils.session_csrf_secret_token() |
@@ -35,21 +35,20 b' from tg import tmpl_context as c' | |||||
35 | from tg.i18n import ugettext as _ |
|
35 | from tg.i18n import ugettext as _ | |
36 | from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound |
|
36 | from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound | |
37 |
|
37 | |||
38 | from kallithea.config.routing import url |
|
38 | import kallithea.lib.helpers as h | |
39 | from kallithea.controllers.changeset import _context_url, _ignorews_url, create_cs_pr_comment, delete_cs_pr_comment |
|
39 | from kallithea.controllers import base | |
40 | from kallithea.lib import diffs |
|
40 | from kallithea.controllers.changeset import create_cs_pr_comment, delete_cs_pr_comment | |
41 |
from kallithea.lib import |
|
41 | from kallithea.lib import auth, diffs, webutils | |
42 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
42 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
43 | from kallithea.lib.base import BaseRepoController, jsonify, render |
|
|||
44 | from kallithea.lib.graphmod import graph_data |
|
43 | from kallithea.lib.graphmod import graph_data | |
45 | from kallithea.lib.page import Page |
|
44 | from kallithea.lib.page import Page | |
46 | from kallithea.lib.utils2 import ascii_bytes, safe_bytes, safe_int |
|
45 | from kallithea.lib.utils2 import ascii_bytes, safe_bytes, safe_int | |
47 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError |
|
46 | from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError | |
|
47 | from kallithea.lib.webutils import url | |||
|
48 | from kallithea.model import db, meta | |||
48 | from kallithea.model.changeset_status import ChangesetStatusModel |
|
49 | from kallithea.model.changeset_status import ChangesetStatusModel | |
49 | from kallithea.model.comment import ChangesetCommentsModel |
|
50 | from kallithea.model.comment import ChangesetCommentsModel | |
50 | from kallithea.model.db import ChangesetStatus, PullRequest, PullRequestReviewer, Repository, User |
|
|||
51 | from kallithea.model.forms import PullRequestForm, PullRequestPostForm |
|
51 | from kallithea.model.forms import PullRequestForm, PullRequestPostForm | |
52 | from kallithea.model.meta import Session |
|
|||
53 | from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel |
|
52 | from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel | |
54 |
|
53 | |||
55 |
|
54 | |||
@@ -59,21 +58,21 b' log = logging.getLogger(__name__)' | |||||
59 | def _get_reviewer(user_id): |
|
58 | def _get_reviewer(user_id): | |
60 | """Look up user by ID and validate it as a potential reviewer.""" |
|
59 | """Look up user by ID and validate it as a potential reviewer.""" | |
61 | try: |
|
60 | try: | |
62 | user = User.get(int(user_id)) |
|
61 | user = db.User.get(int(user_id)) | |
63 | except ValueError: |
|
62 | except ValueError: | |
64 | user = None |
|
63 | user = None | |
65 |
|
64 | |||
66 | if user is None or user.is_default_user: |
|
65 | if user is None or user.is_default_user: | |
67 |
|
|
66 | webutils.flash(_('Invalid reviewer "%s" specified') % user_id, category='error') | |
68 | raise HTTPBadRequest() |
|
67 | raise HTTPBadRequest() | |
69 |
|
68 | |||
70 | return user |
|
69 | return user | |
71 |
|
70 | |||
72 |
|
71 | |||
73 | class PullrequestsController(BaseRepoController): |
|
72 | class PullrequestsController(base.BaseRepoController): | |
74 |
|
73 | |||
75 | def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None): |
|
74 | def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None): | |
76 | """return a structure with repo's interesting changesets, suitable for |
|
75 | """return a structure with scm repo's interesting changesets, suitable for | |
77 | the selectors in pullrequest.html |
|
76 | the selectors in pullrequest.html | |
78 |
|
77 | |||
79 | rev: a revision that must be in the list somehow and selected by default |
|
78 | rev: a revision that must be in the list somehow and selected by default | |
@@ -155,13 +154,14 b' class PullrequestsController(BaseRepoCon' | |||||
155 |
|
154 | |||
156 | # prio 4: tip revision |
|
155 | # prio 4: tip revision | |
157 | if not selected: |
|
156 | if not selected: | |
158 |
if |
|
157 | if repo.alias == 'hg': | |
159 | if tipbranch: |
|
158 | if tipbranch: | |
160 | selected = 'branch:%s:%s' % (tipbranch, tiprev) |
|
159 | selected = 'branch:%s:%s' % (tipbranch, tiprev) | |
161 | else: |
|
160 | else: | |
162 | selected = 'tag:null:' + repo.EMPTY_CHANGESET |
|
161 | selected = 'tag:null:' + repo.EMPTY_CHANGESET | |
163 | tags.append((selected, 'null')) |
|
162 | tags.append((selected, 'null')) | |
164 | else: # Git |
|
163 | else: # Git | |
|
164 | assert repo.alias == 'git' | |||
165 | if not repo.branches: |
|
165 | if not repo.branches: | |
166 | selected = '' # doesn't make sense, but better than nothing |
|
166 | selected = '' # doesn't make sense, but better than nothing | |
167 | elif 'master' in repo.branches: |
|
167 | elif 'master' in repo.branches: | |
@@ -183,9 +183,9 b' class PullrequestsController(BaseRepoCon' | |||||
183 | return False |
|
183 | return False | |
184 |
|
184 | |||
185 | owner = request.authuser.user_id == pull_request.owner_id |
|
185 | owner = request.authuser.user_id == pull_request.owner_id | |
186 | reviewer = PullRequestReviewer.query() \ |
|
186 | reviewer = db.PullRequestReviewer.query() \ | |
187 | .filter(PullRequestReviewer.pull_request == pull_request) \ |
|
187 | .filter(db.PullRequestReviewer.pull_request == pull_request) \ | |
188 | .filter(PullRequestReviewer.user_id == request.authuser.user_id) \ |
|
188 | .filter(db.PullRequestReviewer.user_id == request.authuser.user_id) \ | |
189 | .count() != 0 |
|
189 | .count() != 0 | |
190 |
|
190 | |||
191 | return request.authuser.admin or owner or reviewer |
|
191 | return request.authuser.admin or owner or reviewer | |
@@ -202,7 +202,7 b' class PullrequestsController(BaseRepoCon' | |||||
202 | url_params['closed'] = 1 |
|
202 | url_params['closed'] = 1 | |
203 | p = safe_int(request.GET.get('page'), 1) |
|
203 | p = safe_int(request.GET.get('page'), 1) | |
204 |
|
204 | |||
205 | q = PullRequest.query(include_closed=c.closed, sorted=True) |
|
205 | q = db.PullRequest.query(include_closed=c.closed, sorted=True) | |
206 | if c.from_: |
|
206 | if c.from_: | |
207 | q = q.filter_by(org_repo=c.db_repo) |
|
207 | q = q.filter_by(org_repo=c.db_repo) | |
208 | else: |
|
208 | else: | |
@@ -211,21 +211,21 b' class PullrequestsController(BaseRepoCon' | |||||
211 |
|
211 | |||
212 | c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100, **url_params) |
|
212 | c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100, **url_params) | |
213 |
|
213 | |||
214 | return render('/pullrequests/pullrequest_show_all.html') |
|
214 | return base.render('/pullrequests/pullrequest_show_all.html') | |
215 |
|
215 | |||
216 | @LoginRequired() |
|
216 | @LoginRequired() | |
217 | def show_my(self): |
|
217 | def show_my(self): | |
218 | c.closed = request.GET.get('closed') or '' |
|
218 | c.closed = request.GET.get('closed') or '' | |
219 |
|
219 | |||
220 | c.my_pull_requests = PullRequest.query( |
|
220 | c.my_pull_requests = db.PullRequest.query( | |
221 | include_closed=c.closed, |
|
221 | include_closed=c.closed, | |
222 | sorted=True, |
|
222 | sorted=True, | |
223 | ).filter_by(owner_id=request.authuser.user_id).all() |
|
223 | ).filter_by(owner_id=request.authuser.user_id).all() | |
224 |
|
224 | |||
225 | c.participate_in_pull_requests = [] |
|
225 | c.participate_in_pull_requests = [] | |
226 | c.participate_in_pull_requests_todo = [] |
|
226 | c.participate_in_pull_requests_todo = [] | |
227 | done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED]) |
|
227 | done_status = set([db.ChangesetStatus.STATUS_APPROVED, db.ChangesetStatus.STATUS_REJECTED]) | |
228 | for pr in PullRequest.query( |
|
228 | for pr in db.PullRequest.query( | |
229 | include_closed=c.closed, |
|
229 | include_closed=c.closed, | |
230 | reviewer_id=request.authuser.user_id, |
|
230 | reviewer_id=request.authuser.user_id, | |
231 | sorted=True, |
|
231 | sorted=True, | |
@@ -236,7 +236,7 b' class PullrequestsController(BaseRepoCon' | |||||
236 | else: |
|
236 | else: | |
237 | c.participate_in_pull_requests_todo.append(pr) |
|
237 | c.participate_in_pull_requests_todo.append(pr) | |
238 |
|
238 | |||
239 | return render('/pullrequests/pullrequest_show_my.html') |
|
239 | return base.render('/pullrequests/pullrequest_show_my.html') | |
240 |
|
240 | |||
241 | @LoginRequired() |
|
241 | @LoginRequired() | |
242 | @HasRepoPermissionLevelDecorator('read') |
|
242 | @HasRepoPermissionLevelDecorator('read') | |
@@ -246,7 +246,7 b' class PullrequestsController(BaseRepoCon' | |||||
246 | try: |
|
246 | try: | |
247 | org_scm_instance.get_changeset() |
|
247 | org_scm_instance.get_changeset() | |
248 | except EmptyRepositoryError as e: |
|
248 | except EmptyRepositoryError as e: | |
249 |
|
|
249 | webutils.flash(_('There are no changesets yet'), | |
250 | category='warning') |
|
250 | category='warning') | |
251 | raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name)) |
|
251 | raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name)) | |
252 |
|
252 | |||
@@ -291,11 +291,11 b' class PullrequestsController(BaseRepoCon' | |||||
291 | for fork in org_repo.forks: |
|
291 | for fork in org_repo.forks: | |
292 | c.a_repos.append((fork.repo_name, fork.repo_name)) |
|
292 | c.a_repos.append((fork.repo_name, fork.repo_name)) | |
293 |
|
293 | |||
294 | return render('/pullrequests/pullrequest.html') |
|
294 | return base.render('/pullrequests/pullrequest.html') | |
295 |
|
295 | |||
296 | @LoginRequired() |
|
296 | @LoginRequired() | |
297 | @HasRepoPermissionLevelDecorator('read') |
|
297 | @HasRepoPermissionLevelDecorator('read') | |
298 | @jsonify |
|
298 | @base.jsonify | |
299 | def repo_info(self, repo_name): |
|
299 | def repo_info(self, repo_name): | |
300 | repo = c.db_repo |
|
300 | repo = c.db_repo | |
301 | refs, selected_ref = self._get_repo_refs(repo.scm_instance) |
|
301 | refs, selected_ref = self._get_repo_refs(repo.scm_instance) | |
@@ -315,61 +315,61 b' class PullrequestsController(BaseRepoCon' | |||||
315 | log.error(traceback.format_exc()) |
|
315 | log.error(traceback.format_exc()) | |
316 | log.error(str(errors)) |
|
316 | log.error(str(errors)) | |
317 | msg = _('Error creating pull request: %s') % errors.msg |
|
317 | msg = _('Error creating pull request: %s') % errors.msg | |
318 |
|
|
318 | webutils.flash(msg, 'error') | |
319 | raise HTTPBadRequest |
|
319 | raise HTTPBadRequest | |
320 |
|
320 | |||
321 | # heads up: org and other might seem backward here ... |
|
321 | # heads up: org and other might seem backward here ... | |
322 | org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name |
|
322 | org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name | |
323 | org_repo = Repository.guess_instance(_form['org_repo']) |
|
323 | org_repo = db.Repository.guess_instance(_form['org_repo']) | |
324 |
|
324 | |||
325 | other_ref = _form['other_ref'] # will have symbolic name and head revision |
|
325 | other_ref = _form['other_ref'] # will have symbolic name and head revision | |
326 | other_repo = Repository.guess_instance(_form['other_repo']) |
|
326 | other_repo = db.Repository.guess_instance(_form['other_repo']) | |
327 |
|
327 | |||
328 | reviewers = [] |
|
328 | reviewers = [] | |
329 |
|
329 | |||
330 | title = _form['pullrequest_title'] |
|
330 | title = _form['pullrequest_title'] | |
331 | description = _form['pullrequest_desc'].strip() |
|
331 | description = _form['pullrequest_desc'].strip() | |
332 | owner = User.get(request.authuser.user_id) |
|
332 | owner = db.User.get(request.authuser.user_id) | |
333 |
|
333 | |||
334 | try: |
|
334 | try: | |
335 | cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers) |
|
335 | cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers) | |
336 | except CreatePullRequestAction.ValidationError as e: |
|
336 | except CreatePullRequestAction.ValidationError as e: | |
337 |
|
|
337 | webutils.flash(e, category='error', logf=log.error) | |
338 | raise HTTPNotFound |
|
338 | raise HTTPNotFound | |
339 |
|
339 | |||
340 | try: |
|
340 | try: | |
341 | pull_request = cmd.execute() |
|
341 | pull_request = cmd.execute() | |
342 | Session().commit() |
|
342 | meta.Session().commit() | |
343 | except Exception: |
|
343 | except Exception: | |
344 |
|
|
344 | webutils.flash(_('Error occurred while creating pull request'), | |
345 | category='error') |
|
345 | category='error') | |
346 | log.error(traceback.format_exc()) |
|
346 | log.error(traceback.format_exc()) | |
347 | raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name)) |
|
347 | raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name)) | |
348 |
|
348 | |||
349 |
|
|
349 | webutils.flash(_('Successfully opened new pull request'), | |
350 | category='success') |
|
350 | category='success') | |
351 | raise HTTPFound(location=pull_request.url()) |
|
351 | raise HTTPFound(location=pull_request.url()) | |
352 |
|
352 | |||
353 | def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers): |
|
353 | def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers): | |
354 | owner = User.get(request.authuser.user_id) |
|
354 | owner = db.User.get(request.authuser.user_id) | |
355 | new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev) |
|
355 | new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev) | |
356 | new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1]) |
|
356 | new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1]) | |
357 | try: |
|
357 | try: | |
358 | cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers) |
|
358 | cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers) | |
359 | except CreatePullRequestAction.ValidationError as e: |
|
359 | except CreatePullRequestAction.ValidationError as e: | |
360 |
|
|
360 | webutils.flash(e, category='error', logf=log.error) | |
361 | raise HTTPNotFound |
|
361 | raise HTTPNotFound | |
362 |
|
362 | |||
363 | try: |
|
363 | try: | |
364 | pull_request = cmd.execute() |
|
364 | pull_request = cmd.execute() | |
365 | Session().commit() |
|
365 | meta.Session().commit() | |
366 | except Exception: |
|
366 | except Exception: | |
367 |
|
|
367 | webutils.flash(_('Error occurred while creating pull request'), | |
368 | category='error') |
|
368 | category='error') | |
369 | log.error(traceback.format_exc()) |
|
369 | log.error(traceback.format_exc()) | |
370 | raise HTTPFound(location=old_pull_request.url()) |
|
370 | raise HTTPFound(location=old_pull_request.url()) | |
371 |
|
371 | |||
372 |
|
|
372 | webutils.flash(_('New pull request iteration created'), | |
373 | category='success') |
|
373 | category='success') | |
374 | raise HTTPFound(location=pull_request.url()) |
|
374 | raise HTTPFound(location=pull_request.url()) | |
375 |
|
375 | |||
@@ -377,14 +377,14 b' class PullrequestsController(BaseRepoCon' | |||||
377 | @LoginRequired() |
|
377 | @LoginRequired() | |
378 | @HasRepoPermissionLevelDecorator('read') |
|
378 | @HasRepoPermissionLevelDecorator('read') | |
379 | def post(self, repo_name, pull_request_id): |
|
379 | def post(self, repo_name, pull_request_id): | |
380 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
380 | pull_request = db.PullRequest.get_or_404(pull_request_id) | |
381 | if pull_request.is_closed(): |
|
381 | if pull_request.is_closed(): | |
382 | raise HTTPForbidden() |
|
382 | raise HTTPForbidden() | |
383 | assert pull_request.other_repo.repo_name == repo_name |
|
383 | assert pull_request.other_repo.repo_name == repo_name | |
384 | # only owner or admin can update it |
|
384 | # only owner or admin can update it | |
385 | owner = pull_request.owner_id == request.authuser.user_id |
|
385 | owner = pull_request.owner_id == request.authuser.user_id | |
386 | repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name) |
|
386 | repo_admin = auth.HasRepoPermissionLevel('admin')(c.repo_name) | |
387 | if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner): |
|
387 | if not (auth.HasPermissionAny('hg.admin')() or repo_admin or owner): | |
388 | raise HTTPForbidden() |
|
388 | raise HTTPForbidden() | |
389 |
|
389 | |||
390 | _form = PullRequestPostForm()().to_python(request.POST) |
|
390 | _form = PullRequestPostForm()().to_python(request.POST) | |
@@ -397,11 +397,11 b' class PullrequestsController(BaseRepoCon' | |||||
397 | other_removed = old_reviewers - cur_reviewers |
|
397 | other_removed = old_reviewers - cur_reviewers | |
398 |
|
398 | |||
399 | if other_added: |
|
399 | if other_added: | |
400 |
|
|
400 | webutils.flash(_('Meanwhile, the following reviewers have been added: %s') % | |
401 | (', '.join(u.username for u in other_added)), |
|
401 | (', '.join(u.username for u in other_added)), | |
402 | category='warning') |
|
402 | category='warning') | |
403 | if other_removed: |
|
403 | if other_removed: | |
404 |
|
|
404 | webutils.flash(_('Meanwhile, the following reviewers have been removed: %s') % | |
405 | (', '.join(u.username for u in other_removed)), |
|
405 | (', '.join(u.username for u in other_removed)), | |
406 | category='warning') |
|
406 | category='warning') | |
407 |
|
407 | |||
@@ -418,28 +418,28 b' class PullrequestsController(BaseRepoCon' | |||||
418 | old_description = pull_request.description |
|
418 | old_description = pull_request.description | |
419 | pull_request.title = _form['pullrequest_title'] |
|
419 | pull_request.title = _form['pullrequest_title'] | |
420 | pull_request.description = _form['pullrequest_desc'].strip() or _('No description') |
|
420 | pull_request.description = _form['pullrequest_desc'].strip() or _('No description') | |
421 | pull_request.owner = User.get_by_username(_form['owner']) |
|
421 | pull_request.owner = db.User.get_by_username(_form['owner']) | |
422 | user = User.get(request.authuser.user_id) |
|
422 | user = db.User.get(request.authuser.user_id) | |
423 |
|
423 | |||
424 | PullRequestModel().mention_from_description(user, pull_request, old_description) |
|
424 | PullRequestModel().mention_from_description(user, pull_request, old_description) | |
425 | PullRequestModel().add_reviewers(user, pull_request, added_reviewers) |
|
425 | PullRequestModel().add_reviewers(user, pull_request, added_reviewers) | |
426 | PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers) |
|
426 | PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers) | |
427 |
|
427 | |||
428 | Session().commit() |
|
428 | meta.Session().commit() | |
429 |
|
|
429 | webutils.flash(_('Pull request updated'), category='success') | |
430 |
|
430 | |||
431 | raise HTTPFound(location=pull_request.url()) |
|
431 | raise HTTPFound(location=pull_request.url()) | |
432 |
|
432 | |||
433 | @LoginRequired() |
|
433 | @LoginRequired() | |
434 | @HasRepoPermissionLevelDecorator('read') |
|
434 | @HasRepoPermissionLevelDecorator('read') | |
435 | @jsonify |
|
435 | @base.jsonify | |
436 | def delete(self, repo_name, pull_request_id): |
|
436 | def delete(self, repo_name, pull_request_id): | |
437 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
437 | pull_request = db.PullRequest.get_or_404(pull_request_id) | |
438 | # only owner can delete it ! |
|
438 | # only owner can delete it ! | |
439 | if pull_request.owner_id == request.authuser.user_id: |
|
439 | if pull_request.owner_id == request.authuser.user_id: | |
440 | PullRequestModel().delete(pull_request) |
|
440 | PullRequestModel().delete(pull_request) | |
441 | Session().commit() |
|
441 | meta.Session().commit() | |
442 |
|
|
442 | webutils.flash(_('Successfully deleted pull request'), | |
443 | category='success') |
|
443 | category='success') | |
444 | raise HTTPFound(location=url('my_pullrequests')) |
|
444 | raise HTTPFound(location=url('my_pullrequests')) | |
445 | raise HTTPForbidden() |
|
445 | raise HTTPForbidden() | |
@@ -447,7 +447,7 b' class PullrequestsController(BaseRepoCon' | |||||
447 | @LoginRequired(allow_default_user=True) |
|
447 | @LoginRequired(allow_default_user=True) | |
448 | @HasRepoPermissionLevelDecorator('read') |
|
448 | @HasRepoPermissionLevelDecorator('read') | |
449 | def show(self, repo_name, pull_request_id, extra=None): |
|
449 | def show(self, repo_name, pull_request_id, extra=None): | |
450 | c.pull_request = PullRequest.get_or_404(pull_request_id) |
|
450 | c.pull_request = db.PullRequest.get_or_404(pull_request_id) | |
451 | c.allowed_to_change_status = self._is_allowed_to_change_status(c.pull_request) |
|
451 | c.allowed_to_change_status = self._is_allowed_to_change_status(c.pull_request) | |
452 | cc_model = ChangesetCommentsModel() |
|
452 | cc_model = ChangesetCommentsModel() | |
453 | cs_model = ChangesetStatusModel() |
|
453 | cs_model = ChangesetStatusModel() | |
@@ -475,7 +475,7 b' class PullrequestsController(BaseRepoCon' | |||||
475 | c.cs_ranges.append(org_scm_instance.get_changeset(x)) |
|
475 | c.cs_ranges.append(org_scm_instance.get_changeset(x)) | |
476 | except ChangesetDoesNotExistError: |
|
476 | except ChangesetDoesNotExistError: | |
477 | c.cs_ranges = [] |
|
477 | c.cs_ranges = [] | |
478 |
|
|
478 | webutils.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name), | |
479 | 'error') |
|
479 | 'error') | |
480 | break |
|
480 | break | |
481 | c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ... |
|
481 | c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ... | |
@@ -553,7 +553,7 b' class PullrequestsController(BaseRepoCon' | |||||
553 | show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name)) |
|
553 | show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name)) | |
554 | show.add(revs[0]) # make sure graph shows this so we can see how they relate |
|
554 | show.add(revs[0]) # make sure graph shows this so we can see how they relate | |
555 | c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name, |
|
555 | c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name, | |
556 |
|
|
556 | org_scm_instance.get_changeset(max(brevs)).short_id) | |
557 |
|
557 | |||
558 | avail_show = sorted(show, reverse=True) |
|
558 | avail_show = sorted(show, reverse=True) | |
559 |
|
559 | |||
@@ -571,10 +571,8 b' class PullrequestsController(BaseRepoCon' | |||||
571 | c.cs_comments = c.cs_repo.get_comments(raw_ids) |
|
571 | c.cs_comments = c.cs_repo.get_comments(raw_ids) | |
572 | c.cs_statuses = c.cs_repo.statuses(raw_ids) |
|
572 | c.cs_statuses = c.cs_repo.statuses(raw_ids) | |
573 |
|
573 | |||
574 | ignore_whitespace = request.GET.get('ignorews') == '1' |
|
574 | ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET) | |
575 | line_context = safe_int(request.GET.get('context'), 3) |
|
575 | diff_context_size = h.get_diff_context_size(request.GET) | |
576 | c.ignorews_url = _ignorews_url |
|
|||
577 | c.context_url = _context_url |
|
|||
578 | fulldiff = request.GET.get('fulldiff') |
|
576 | fulldiff = request.GET.get('fulldiff') | |
579 | diff_limit = None if fulldiff else self.cut_off_limit |
|
577 | diff_limit = None if fulldiff else self.cut_off_limit | |
580 |
|
578 | |||
@@ -583,7 +581,7 b' class PullrequestsController(BaseRepoCon' | |||||
583 | c.a_rev, c.cs_rev, org_scm_instance.path) |
|
581 | c.a_rev, c.cs_rev, org_scm_instance.path) | |
584 | try: |
|
582 | try: | |
585 | raw_diff = diffs.get_diff(org_scm_instance, rev1=c.a_rev, rev2=c.cs_rev, |
|
583 | raw_diff = diffs.get_diff(org_scm_instance, rev1=c.a_rev, rev2=c.cs_rev, | |
586 |
ignore_whitespace=ignore_whitespace, context= |
|
584 | ignore_whitespace=ignore_whitespace_diff, context=diff_context_size) | |
587 | except ChangesetDoesNotExistError: |
|
585 | except ChangesetDoesNotExistError: | |
588 | raw_diff = safe_bytes(_("The diff can't be shown - the PR revisions could not be found.")) |
|
586 | raw_diff = safe_bytes(_("The diff can't be shown - the PR revisions could not be found.")) | |
589 | diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit) |
|
587 | diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit) | |
@@ -598,7 +596,7 b' class PullrequestsController(BaseRepoCon' | |||||
598 | c.lines_deleted += st['deleted'] |
|
596 | c.lines_deleted += st['deleted'] | |
599 | filename = f['filename'] |
|
597 | filename = f['filename'] | |
600 | fid = h.FID('', filename) |
|
598 | fid = h.FID('', filename) | |
601 |
html_diff = diffs.as_html( |
|
599 | html_diff = diffs.as_html(parsed_lines=[f]) | |
602 | c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st)) |
|
600 | c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st)) | |
603 |
|
601 | |||
604 | # inline comments |
|
602 | # inline comments | |
@@ -618,23 +616,23 b' class PullrequestsController(BaseRepoCon' | |||||
618 | c.pull_request_pending_reviewers, |
|
616 | c.pull_request_pending_reviewers, | |
619 | c.current_voting_result, |
|
617 | c.current_voting_result, | |
620 | ) = cs_model.calculate_pull_request_result(c.pull_request) |
|
618 | ) = cs_model.calculate_pull_request_result(c.pull_request) | |
621 | c.changeset_statuses = ChangesetStatus.STATUSES |
|
619 | c.changeset_statuses = db.ChangesetStatus.STATUSES | |
622 |
|
620 | |||
623 | c.is_ajax_preview = False |
|
621 | c.is_ajax_preview = False | |
624 | c.ancestors = None # [c.a_rev] ... but that is shown in an other way |
|
622 | c.ancestors = None # [c.a_rev] ... but that is shown in an other way | |
625 | return render('/pullrequests/pullrequest_show.html') |
|
623 | return base.render('/pullrequests/pullrequest_show.html') | |
626 |
|
624 | |||
627 | @LoginRequired() |
|
625 | @LoginRequired() | |
628 | @HasRepoPermissionLevelDecorator('read') |
|
626 | @HasRepoPermissionLevelDecorator('read') | |
629 | @jsonify |
|
627 | @base.jsonify | |
630 | def comment(self, repo_name, pull_request_id): |
|
628 | def comment(self, repo_name, pull_request_id): | |
631 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
629 | pull_request = db.PullRequest.get_or_404(pull_request_id) | |
632 | allowed_to_change_status = self._is_allowed_to_change_status(pull_request) |
|
630 | allowed_to_change_status = self._is_allowed_to_change_status(pull_request) | |
633 | return create_cs_pr_comment(repo_name, pull_request=pull_request, |
|
631 | return create_cs_pr_comment(repo_name, pull_request=pull_request, | |
634 | allowed_to_change_status=allowed_to_change_status) |
|
632 | allowed_to_change_status=allowed_to_change_status) | |
635 |
|
633 | |||
636 | @LoginRequired() |
|
634 | @LoginRequired() | |
637 | @HasRepoPermissionLevelDecorator('read') |
|
635 | @HasRepoPermissionLevelDecorator('read') | |
638 | @jsonify |
|
636 | @base.jsonify | |
639 | def delete_comment(self, repo_name, comment_id): |
|
637 | def delete_comment(self, repo_name, comment_id): | |
640 | return delete_cs_pr_comment(repo_name, comment_id) |
|
638 | return delete_cs_pr_comment(repo_name, comment_id) |
@@ -14,9 +14,9 b'' | |||||
14 | from tg import config |
|
14 | from tg import config | |
15 | from tgext.routes import RoutedController |
|
15 | from tgext.routes import RoutedController | |
16 |
|
16 | |||
17 |
from kallithea.con |
|
17 | from kallithea.controllers import base | |
18 | from kallithea.controllers.error import ErrorController |
|
18 | from kallithea.controllers.error import ErrorController | |
19 | from kallithea.lib.base import BaseController |
|
19 | from kallithea.controllers.routing import make_map | |
20 |
|
20 | |||
21 |
|
21 | |||
22 | # This is the main Kallithea entry point; TurboGears will forward all requests |
|
22 | # This is the main Kallithea entry point; TurboGears will forward all requests | |
@@ -26,7 +26,7 b' from kallithea.lib.base import BaseContr' | |||||
26 | # The mapper is configured using routes defined in routing.py. This use of the |
|
26 | # The mapper is configured using routes defined in routing.py. This use of the | |
27 | # 'mapper' attribute is a feature of tgext.routes, which is activated by |
|
27 | # 'mapper' attribute is a feature of tgext.routes, which is activated by | |
28 | # inheriting from its RoutedController class. |
|
28 | # inheriting from its RoutedController class. | |
29 | class RootController(RoutedController, BaseController): |
|
29 | class RootController(RoutedController, base.BaseController): | |
30 |
|
30 | |||
31 | def __init__(self): |
|
31 | def __init__(self): | |
32 | self.mapper = make_map(config) |
|
32 | self.mapper = make_map(config) |
@@ -20,15 +20,12 b' refer to the routes manual at http://rou' | |||||
20 | """ |
|
20 | """ | |
21 |
|
21 | |||
22 | import routes |
|
22 | import routes | |
23 | from tg import request |
|
|||
24 |
|
23 | |||
|
24 | import kallithea | |||
|
25 | from kallithea.lib.utils import is_valid_repo, is_valid_repo_group | |||
25 | from kallithea.lib.utils2 import safe_str |
|
26 | from kallithea.lib.utils2 import safe_str | |
26 |
|
27 | |||
27 |
|
28 | |||
28 | # prefix for non repository related links needs to be prefixed with `/` |
|
|||
29 | ADMIN_PREFIX = '/_admin' |
|
|||
30 |
|
||||
31 |
|
||||
32 | class Mapper(routes.Mapper): |
|
29 | class Mapper(routes.Mapper): | |
33 | """ |
|
30 | """ | |
34 | Subclassed Mapper with routematch patched to decode "unicode" str url to |
|
31 | Subclassed Mapper with routematch patched to decode "unicode" str url to | |
@@ -54,8 +51,6 b' def make_map(config):' | |||||
54 | rmap.minimization = False |
|
51 | rmap.minimization = False | |
55 | rmap.explicit = False |
|
52 | rmap.explicit = False | |
56 |
|
53 | |||
57 | from kallithea.lib.utils import is_valid_repo, is_valid_repo_group |
|
|||
58 |
|
||||
59 | def check_repo(environ, match_dict): |
|
54 | def check_repo(environ, match_dict): | |
60 | """ |
|
55 | """ | |
61 | Check for valid repository for proper 404 handling. |
|
56 | Check for valid repository for proper 404 handling. | |
@@ -121,6 +116,7 b' def make_map(config):' | |||||
121 | rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True) |
|
116 | rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True) | |
122 |
|
117 | |||
123 | # ADMIN REPOSITORY ROUTES |
|
118 | # ADMIN REPOSITORY ROUTES | |
|
119 | ADMIN_PREFIX = kallithea.ADMIN_PREFIX | |||
124 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
120 | with rmap.submapper(path_prefix=ADMIN_PREFIX, | |
125 | controller='admin/repos') as m: |
|
121 | controller='admin/repos') as m: | |
126 | m.connect("repos", "/repos", |
|
122 | m.connect("repos", "/repos", | |
@@ -775,29 +771,3 b' def make_map(config):' | |||||
775 | conditions=dict(function=check_repo)) |
|
771 | conditions=dict(function=check_repo)) | |
776 |
|
772 | |||
777 | return rmap |
|
773 | return rmap | |
778 |
|
||||
779 |
|
||||
780 | class UrlGenerator(object): |
|
|||
781 | """Emulate pylons.url in providing a wrapper around routes.url |
|
|||
782 |
|
||||
783 | This code was added during migration from Pylons to Turbogears2. Pylons |
|
|||
784 | already provided a wrapper like this, but Turbogears2 does not. |
|
|||
785 |
|
||||
786 | When the routing of Kallithea is changed to use less Routes and more |
|
|||
787 | Turbogears2-style routing, this class may disappear or change. |
|
|||
788 |
|
||||
789 | url() (the __call__ method) returns the URL based on a route name and |
|
|||
790 | arguments. |
|
|||
791 | url.current() returns the URL of the current page with arguments applied. |
|
|||
792 |
|
||||
793 | Refer to documentation of Routes for details: |
|
|||
794 | https://routes.readthedocs.io/en/latest/generating.html#generation |
|
|||
795 | """ |
|
|||
796 | def __call__(self, *args, **kwargs): |
|
|||
797 | return request.environ['routes.url'](*args, **kwargs) |
|
|||
798 |
|
||||
799 | def current(self, *args, **kwargs): |
|
|||
800 | return request.environ['routes.url'].current(*args, **kwargs) |
|
|||
801 |
|
||||
802 |
|
||||
803 | url = UrlGenerator() |
|
@@ -35,8 +35,8 b' from whoosh.index import EmptyIndexError' | |||||
35 | from whoosh.qparser import QueryParser, QueryParserError |
|
35 | from whoosh.qparser import QueryParser, QueryParserError | |
36 | from whoosh.query import Phrase, Prefix |
|
36 | from whoosh.query import Phrase, Prefix | |
37 |
|
37 | |||
|
38 | from kallithea.controllers import base | |||
38 | from kallithea.lib.auth import LoginRequired |
|
39 | from kallithea.lib.auth import LoginRequired | |
39 | from kallithea.lib.base import BaseRepoController, render |
|
|||
40 | from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA, WhooshResultWrapper |
|
40 | from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA, WhooshResultWrapper | |
41 | from kallithea.lib.page import Page |
|
41 | from kallithea.lib.page import Page | |
42 | from kallithea.lib.utils2 import safe_int |
|
42 | from kallithea.lib.utils2 import safe_int | |
@@ -46,7 +46,7 b' from kallithea.model.repo import RepoMod' | |||||
46 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
47 |
|
47 | |||
48 |
|
48 | |||
49 | class SearchController(BaseRepoController): |
|
49 | class SearchController(base.BaseRepoController): | |
50 |
|
50 | |||
51 | @LoginRequired(allow_default_user=True) |
|
51 | @LoginRequired(allow_default_user=True) | |
52 | def index(self, repo_name=None): |
|
52 | def index(self, repo_name=None): | |
@@ -139,4 +139,4 b' class SearchController(BaseRepoControlle' | |||||
139 | c.runtime = _('An error occurred during search operation.') |
|
139 | c.runtime = _('An error occurred during search operation.') | |
140 |
|
140 | |||
141 | # Return a rendered template |
|
141 | # Return a rendered template | |
142 | return render('/search/search.html') |
|
142 | return base.render('/search/search.html') |
@@ -38,19 +38,17 b' from tg import tmpl_context as c' | |||||
38 | from tg.i18n import ugettext as _ |
|
38 | from tg.i18n import ugettext as _ | |
39 | from webob.exc import HTTPBadRequest |
|
39 | from webob.exc import HTTPBadRequest | |
40 |
|
40 | |||
41 | import kallithea.lib.helpers as h |
|
41 | from kallithea.controllers import base | |
42 | from kallithea.config.conf import ALL_EXTS, ALL_READMES, LANGUAGES_EXTENSIONS_MAP |
|
42 | from kallithea.lib import ext_json, webutils | |
43 | from kallithea.lib import ext_json |
|
|||
44 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired |
|
43 | from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired | |
45 | from kallithea.lib.base import BaseRepoController, jsonify, render |
|
44 | from kallithea.lib.conf import ALL_EXTS, ALL_READMES, LANGUAGES_EXTENSIONS_MAP | |
46 | from kallithea.lib.celerylib.tasks import get_commits_stats |
|
|||
47 | from kallithea.lib.markup_renderer import MarkupRenderer |
|
45 | from kallithea.lib.markup_renderer import MarkupRenderer | |
48 | from kallithea.lib.page import Page |
|
46 | from kallithea.lib.page import Page | |
49 | from kallithea.lib.utils2 import safe_int, safe_str |
|
47 | from kallithea.lib.utils2 import safe_int, safe_str | |
50 | from kallithea.lib.vcs.backends.base import EmptyChangeset |
|
48 | from kallithea.lib.vcs.backends.base import EmptyChangeset | |
51 | from kallithea.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, NodeDoesNotExistError |
|
49 | from kallithea.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, NodeDoesNotExistError | |
52 | from kallithea.lib.vcs.nodes import FileNode |
|
50 | from kallithea.lib.vcs.nodes import FileNode | |
53 |
from kallithea.model |
|
51 | from kallithea.model import async_tasks, db | |
54 |
|
52 | |||
55 |
|
53 | |||
56 | log = logging.getLogger(__name__) |
|
54 | log = logging.getLogger(__name__) | |
@@ -60,7 +58,7 b" README_FILES = [''.join([x[0][0], x[1][0" | |||||
60 | key=lambda y:y[0][1] + y[1][1])] |
|
58 | key=lambda y:y[0][1] + y[1][1])] | |
61 |
|
59 | |||
62 |
|
60 | |||
63 | class SummaryController(BaseRepoController): |
|
61 | class SummaryController(base.BaseRepoController): | |
64 |
|
62 | |||
65 | def __get_readme_data(self, db_repo): |
|
63 | def __get_readme_data(self, db_repo): | |
66 | repo_name = db_repo.repo_name |
|
64 | repo_name = db_repo.repo_name | |
@@ -108,7 +106,7 b' class SummaryController(BaseRepoControll' | |||||
108 | try: |
|
106 | try: | |
109 | collection = c.db_repo_scm_instance.get_changesets(reverse=True) |
|
107 | collection = c.db_repo_scm_instance.get_changesets(reverse=True) | |
110 | except EmptyRepositoryError as e: |
|
108 | except EmptyRepositoryError as e: | |
111 |
|
|
109 | webutils.flash(e, category='warning') | |
112 | collection = [] |
|
110 | collection = [] | |
113 | c.cs_pagination = Page(collection, page=p, items_per_page=size) |
|
111 | c.cs_pagination = Page(collection, page=p, items_per_page=size) | |
114 | page_revisions = [x.raw_id for x in list(c.cs_pagination)] |
|
112 | page_revisions = [x.raw_id for x in list(c.cs_pagination)] | |
@@ -131,8 +129,8 b' class SummaryController(BaseRepoControll' | |||||
131 | else: |
|
129 | else: | |
132 | c.show_stats = False |
|
130 | c.show_stats = False | |
133 |
|
131 | |||
134 | stats = Statistics.query() \ |
|
132 | stats = db.Statistics.query() \ | |
135 | .filter(Statistics.repository == c.db_repo) \ |
|
133 | .filter(db.Statistics.repository == c.db_repo) \ | |
136 | .scalar() |
|
134 | .scalar() | |
137 |
|
135 | |||
138 | c.stats_percentage = 0 |
|
136 | c.stats_percentage = 0 | |
@@ -150,11 +148,11 b' class SummaryController(BaseRepoControll' | |||||
150 | c.enable_downloads = c.db_repo.enable_downloads |
|
148 | c.enable_downloads = c.db_repo.enable_downloads | |
151 | c.readme_data, c.readme_file = \ |
|
149 | c.readme_data, c.readme_file = \ | |
152 | self.__get_readme_data(c.db_repo) |
|
150 | self.__get_readme_data(c.db_repo) | |
153 | return render('summary/summary.html') |
|
151 | return base.render('summary/summary.html') | |
154 |
|
152 | |||
155 | @LoginRequired() |
|
153 | @LoginRequired() | |
156 | @HasRepoPermissionLevelDecorator('read') |
|
154 | @HasRepoPermissionLevelDecorator('read') | |
157 | @jsonify |
|
155 | @base.jsonify | |
158 | def repo_size(self, repo_name): |
|
156 | def repo_size(self, repo_name): | |
159 | if request.is_xhr: |
|
157 | if request.is_xhr: | |
160 | return c.db_repo._repo_size() |
|
158 | return c.db_repo._repo_size() | |
@@ -181,8 +179,8 b' class SummaryController(BaseRepoControll' | |||||
181 | c.ts_min = ts_min_m |
|
179 | c.ts_min = ts_min_m | |
182 | c.ts_max = ts_max_y |
|
180 | c.ts_max = ts_max_y | |
183 |
|
181 | |||
184 | stats = Statistics.query() \ |
|
182 | stats = db.Statistics.query() \ | |
185 | .filter(Statistics.repository == c.db_repo) \ |
|
183 | .filter(db.Statistics.repository == c.db_repo) \ | |
186 | .scalar() |
|
184 | .scalar() | |
187 | c.stats_percentage = 0 |
|
185 | c.stats_percentage = 0 | |
188 | if stats and stats.languages: |
|
186 | if stats and stats.languages: | |
@@ -210,5 +208,5 b' class SummaryController(BaseRepoControll' | |||||
210 | c.trending_languages = [] |
|
208 | c.trending_languages = [] | |
211 |
|
209 | |||
212 | recurse_limit = 500 # don't recurse more than 500 times when parsing |
|
210 | recurse_limit = 500 # don't recurse more than 500 times when parsing | |
213 | get_commits_stats(c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit) |
|
211 | async_tasks.get_commits_stats(c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit) | |
214 | return render('summary/statistics.html') |
|
212 | return base.render('summary/statistics.html') |
@@ -62,6 +62,7 b' BIN_FILENODE = 6' | |||||
62 | border-collapse: collapse; |
|
62 | border-collapse: collapse; | |
63 | border-radius: 0px !important; |
|
63 | border-radius: 0px !important; | |
64 | width: 100%; |
|
64 | width: 100%; | |
|
65 | table-layout: fixed; | |||
65 |
|
66 | |||
66 | /* line coloring */ |
|
67 | /* line coloring */ | |
67 | .context { |
|
68 | .context { | |
@@ -105,31 +106,26 b' BIN_FILENODE = 6' | |||||
105 | border-color: rgba(0, 0, 0, 0.3); |
|
106 | border-color: rgba(0, 0, 0, 0.3); | |
106 | } |
|
107 | } | |
107 |
|
108 | |||
108 | /* line numbers */ |
|
109 | /* line number columns */ | |
109 | .lineno { |
|
110 | td.lineno { | |
110 | padding-left: 2px; |
|
111 | width: 4em; | |
111 | padding-right: 2px !important; |
|
|||
112 | width: 30px; |
|
|||
113 | border-right: 1px solid @panel-default-border !important; |
|
112 | border-right: 1px solid @panel-default-border !important; | |
114 | vertical-align: middle !important; |
|
113 | vertical-align: middle !important; | |
115 | text-align: center; |
|
|||
116 | } |
|
|||
117 | .lineno.new { |
|
|||
118 | text-align: right; |
|
|||
119 | } |
|
|||
120 | .lineno.old { |
|
|||
121 | text-align: right; |
|
|||
122 | } |
|
|||
123 | .lineno a { |
|
|||
124 | color: #aaa !important; |
|
|||
125 | font-size: 11px; |
|
114 | font-size: 11px; | |
126 | font-family: @font-family-monospace; |
|
115 | font-family: @font-family-monospace; | |
127 | line-height: normal; |
|
116 | line-height: normal; | |
128 | padding-left: 6px; |
|
117 | text-align: center; | |
129 | padding-right: 6px; |
|
118 | } | |
130 | display: block; |
|
119 | td.lineno[colspan="2"] { | |
|
120 | width: 8em; | |||
131 | } |
|
121 | } | |
132 |
|
|
122 | td.lineno a { | |
|
123 | color: #aaa !important; | |||
|
124 | display: inline-block; | |||
|
125 | min-width: 2em; | |||
|
126 | text-align: right; | |||
|
127 | } | |||
|
128 | tr.line:hover td.lineno a { | |||
133 | color: #333 !important; |
|
129 | color: #333 !important; | |
134 | } |
|
130 | } | |
135 | /** CODE **/ |
|
131 | /** CODE **/ | |
@@ -172,27 +168,24 b' BIN_FILENODE = 6' | |||||
172 | left: -8px; |
|
168 | left: -8px; | |
173 | box-sizing: border-box; |
|
169 | box-sizing: border-box; | |
174 | } |
|
170 | } | |
175 | /* comment bubble, only visible when in a commentable diff */ |
|
171 | .commentable-diff tr.line:hover td .add-bubble { | |
176 | .commentable-diff tr.line.add:hover td .add-bubble, |
|
|||
177 | .commentable-diff tr.line.del:hover td .add-bubble, |
|
|||
178 | .commentable-diff tr.line.unmod:hover td .add-bubble { |
|
|||
179 | display: block; |
|
172 | display: block; | |
180 | z-index: 1; |
|
173 | z-index: 1; | |
181 | } |
|
174 | } | |
182 | .add-bubble div { |
|
175 | .add-bubble div { | |
183 | background: @kallithea-theme-main-color; |
|
176 | background: @kallithea-theme-main-color; | |
184 |
width: 1 |
|
177 | width: 1.2em; | |
185 |
height: 1 |
|
178 | height: 1.2em; | |
186 |
line-height: 1 |
|
179 | line-height: 1em; | |
187 | cursor: pointer; |
|
180 | cursor: pointer; | |
188 | padding: 0 2px 2px 0.5px; |
|
181 | padding: 0.1em 0.1em 0.1em 0.12em; | |
189 | border: 1px solid @kallithea-theme-main-color; |
|
182 | border: 1px solid @kallithea-theme-main-color; | |
190 |
border-radius: |
|
183 | border-radius: 0.2em; | |
191 | box-sizing: border-box; |
|
184 | box-sizing: border-box; | |
192 | overflow: hidden; |
|
185 | overflow: hidden; | |
193 | } |
|
186 | } | |
194 | .add-bubble div:before { |
|
187 | .add-bubble div:before { | |
195 |
font-size: 1 |
|
188 | font-size: 1em; | |
196 | color: #ffffff; |
|
189 | color: #ffffff; | |
197 | font-family: "kallithea"; |
|
190 | font-family: "kallithea"; | |
198 | content: '\1f5ea'; |
|
191 | content: '\1f5ea'; |
@@ -564,10 +564,6 b' input.status_change_radio {' | |||||
564 | background-position: 20px 0; |
|
564 | background-position: 20px 0; | |
565 | } |
|
565 | } | |
566 | } |
|
566 | } | |
567 | .comment-preview.failed .user, |
|
|||
568 | .comment-preview.failed .panel-body { |
|
|||
569 | color: #666; |
|
|||
570 | } |
|
|||
571 | .comment-preview .comment-submission-status { |
|
567 | .comment-preview .comment-submission-status { | |
572 | float: right; |
|
568 | float: right; | |
573 | } |
|
569 | } |
@@ -11,24 +11,21 b' msgstr ""' | |||||
11 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" |
|
11 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | |
12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" |
|
12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" | |
13 |
|
13 | |||
|
14 | msgid "Repository not found in the filesystem" | |||
|
15 | msgstr "Рэпазітар не знойдзены на файлавай сістэме" | |||
|
16 | ||||
14 | msgid "There are no changesets yet" |
|
17 | msgid "There are no changesets yet" | |
15 | msgstr "Яшчэ не было змен" |
|
18 | msgstr "Яшчэ не было змен" | |
16 |
|
19 | |||
|
20 | msgid "Changeset for %s %s not found in %s" | |||
|
21 | msgstr "Набор змен для %s %s не знойдзены ў %s" | |||
|
22 | ||||
17 | msgid "None" |
|
23 | msgid "None" | |
18 | msgstr "Нічога" |
|
24 | msgstr "Нічога" | |
19 |
|
25 | |||
20 | msgid "(closed)" |
|
26 | msgid "(closed)" | |
21 | msgstr "(зачынена)" |
|
27 | msgstr "(зачынена)" | |
22 |
|
28 | |||
23 | msgid "Show whitespace" |
|
|||
24 | msgstr "Паказваць прабелы" |
|
|||
25 |
|
||||
26 | msgid "Ignore whitespace" |
|
|||
27 | msgstr "Ігнараваць прабелы" |
|
|||
28 |
|
||||
29 | msgid "Increase diff context to %(num)s lines" |
|
|||
30 | msgstr "Павялічыць кантэкст да %(num)s радкоў" |
|
|||
31 |
|
||||
32 | msgid "Successfully deleted pull request %s" |
|
29 | msgid "Successfully deleted pull request %s" | |
33 | msgstr "Pull-запыт %s паспяхова выдалены" |
|
30 | msgstr "Pull-запыт %s паспяхова выдалены" | |
34 |
|
31 | |||
@@ -462,13 +459,6 b' msgstr "\xd0\x90\xd0\xb4\xd0\xb1\xd1\x8b\xd0\xbb\xd0\xb0\xd1\x81\xd1\x8f \xd0\xbf\xd0\xb0\xd0\xbc\xd1\x8b\xd0\xbb\xd0\xba\xd0\xb0 \xd0\xbf\xd1\x80\xd1\x8b \xd0\xb2\xd1\x8b\xd0\xb4\xd0\xb0\xd0\xbb\xd0\xb5\xd0\xbd\xd0\xbd\xd1\x96 \xd1\x81\xd1\x82\xd0\xb0\xd1\x82\xd1\x8b\xd1\x81\xd1\x82\xd1\x8b\xd0\xba\xd1\x96 \xd1\x80\xd1\x8d\xd0\xbf\xd0\xb0\xd0\xb7\xd1\x96\xd1\x82\xd0\xb0\xd1\x80\xd0\xb0"' | |||||
462 | msgid "Updated VCS settings" |
|
459 | msgid "Updated VCS settings" | |
463 | msgstr "Абноўлены налады VCS" |
|
460 | msgstr "Абноўлены налады VCS" | |
464 |
|
461 | |||
465 | msgid "" |
|
|||
466 | "Unable to activate hgsubversion support. The \"hgsubversion\" library is " |
|
|||
467 | "missing" |
|
|||
468 | msgstr "" |
|
|||
469 | "Немагчыма ўключыць падтрымку hgsubversion. Бібліятэка hgsubversion " |
|
|||
470 | "адсутнічае" |
|
|||
471 |
|
||||
472 | msgid "Error occurred while updating application settings" |
|
462 | msgid "Error occurred while updating application settings" | |
473 | msgstr "Памылка пры абнаўленні наладаў праграмы" |
|
463 | msgstr "Памылка пры абнаўленні наладаў праграмы" | |
474 |
|
464 | |||
@@ -566,12 +556,6 b' msgstr ""' | |||||
566 | msgid "You need to be signed in to view this page" |
|
556 | msgid "You need to be signed in to view this page" | |
567 | msgstr "Старонка даступная толькі аўтарызаваным карыстальнікам" |
|
557 | msgstr "Старонка даступная толькі аўтарызаваным карыстальнікам" | |
568 |
|
558 | |||
569 | msgid "Repository not found in the filesystem" |
|
|||
570 | msgstr "Рэпазітар не знойдзены на файлавай сістэме" |
|
|||
571 |
|
||||
572 | msgid "Changeset for %s %s not found in %s" |
|
|||
573 | msgstr "Набор змен для %s %s не знойдзены ў %s" |
|
|||
574 |
|
||||
575 | msgid "Binary file" |
|
559 | msgid "Binary file" | |
576 | msgstr "Двайковы файл" |
|
560 | msgstr "Двайковы файл" | |
577 |
|
561 | |||
@@ -584,6 +568,9 b' msgstr ""' | |||||
584 | msgid "No changes detected" |
|
568 | msgid "No changes detected" | |
585 | msgstr "Змен не выяўлена" |
|
569 | msgstr "Змен не выяўлена" | |
586 |
|
570 | |||
|
571 | msgid "Increase diff context to %(num)s lines" | |||
|
572 | msgstr "Павялічыць кантэкст да %(num)s радкоў" | |||
|
573 | ||||
587 | msgid "Deleted branch: %s" |
|
574 | msgid "Deleted branch: %s" | |
588 | msgstr "Выдаленая галіна: %s" |
|
575 | msgstr "Выдаленая галіна: %s" | |
589 |
|
576 | |||
@@ -695,15 +682,6 b' msgstr "\xd0\xbf\xd0\xb5\xd1\x80\xd0\xb0\xd0\xbd\xd0\xb0\xd0\xb7\xd0\xb2\xd0\xb0\xd0\xbd\xd1\x8b"' | |||||
695 | msgid "chmod" |
|
682 | msgid "chmod" | |
696 | msgstr "chmod" |
|
683 | msgstr "chmod" | |
697 |
|
684 | |||
698 | msgid "" |
|
|||
699 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
700 | "the filesystem please run the application again in order to rescan " |
|
|||
701 | "repositories" |
|
|||
702 | msgstr "" |
|
|||
703 | "Рэпазітар %s адсутнічае ў базе дадзеных; магчыма, ён быў створаны ці " |
|
|||
704 | "пераназваны з файлавай сістэмы. Калі ласка, перазапусціце прыкладанне для " |
|
|||
705 | "сканавання рэпазітароў" |
|
|||
706 |
|
||||
707 | msgid "%d year" |
|
685 | msgid "%d year" | |
708 | msgid_plural "%d years" |
|
686 | msgid_plural "%d years" | |
709 | msgstr[0] "%d год" |
|
687 | msgstr[0] "%d год" | |
@@ -755,24 +733,12 b' msgstr "%s \xd1\x96 %s \xd0\xbd\xd0\xb0\xd0\xb7\xd0\xb0\xd0\xb4"' | |||||
755 | msgid "just now" |
|
733 | msgid "just now" | |
756 | msgstr "цяпер" |
|
734 | msgstr "цяпер" | |
757 |
|
735 | |||
758 | msgid "on line %s" |
|
|||
759 | msgstr "на радку %s" |
|
|||
760 |
|
||||
761 | msgid "[Mention]" |
|
|||
762 | msgstr "[Згадванне]" |
|
|||
763 |
|
||||
764 | msgid "top level" |
|
736 | msgid "top level" | |
765 | msgstr "верхні ўзровень" |
|
737 | msgstr "верхні ўзровень" | |
766 |
|
738 | |||
767 | msgid "Kallithea Administrator" |
|
739 | msgid "Kallithea Administrator" | |
768 | msgstr "Адміністратар Kallithea" |
|
740 | msgstr "Адміністратар Kallithea" | |
769 |
|
741 | |||
770 | msgid "Only admins can create repository groups" |
|
|||
771 | msgstr "Толькі адміністратары могуць ствараць групы репазітароў" |
|
|||
772 |
|
||||
773 | msgid "Non-admins can create repository groups" |
|
|||
774 | msgstr "Неадміністратары могуць ствараць групы репазітароў" |
|
|||
775 |
|
||||
776 | msgid "Only admins can create user groups" |
|
742 | msgid "Only admins can create user groups" | |
777 | msgstr "Толькі адміністратары могуць ствараць групы карыстальнікаў" |
|
743 | msgstr "Толькі адміністратары могуць ствараць групы карыстальнікаў" | |
778 |
|
744 | |||
@@ -827,17 +793,9 b' msgstr "\xd0\x9d\xd0\xbe\xd0\xb2\xd1\x8b \xd0\xba\xd0\xb0\xd1\x80\xd1\x8b\xd1\x81\xd1\x82\xd0\xb0\xd0\xbb\xd1\x8c\xd0\xbd\xd1\x96\xd0\xba \\"%(new_username)s\\" \xd0\xb7\xd0\xb0\xd1\x80\xd1\x8d\xd0\xb3\xd1\x96\xd1\x81\xd1\x82\xd1\x80\xd0\xb0\xd0\xb2\xd0\xb0\xd0\xbd\xd1\x8b"' | |||||
827 | msgid "Closing" |
|
793 | msgid "Closing" | |
828 | msgstr "Зачынены" |
|
794 | msgstr "Зачынены" | |
829 |
|
795 | |||
830 | msgid "" |
|
|||
831 | "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s" |
|
|||
832 | msgstr "" |
|
|||
833 | "%(user)s просіць вас разгледзець pull request %(pr_nice_id)s: %(pr_title)s" |
|
|||
834 |
|
||||
835 | msgid "latest tip" |
|
796 | msgid "latest tip" | |
836 | msgstr "апошняя версія" |
|
797 | msgstr "апошняя версія" | |
837 |
|
798 | |||
838 | msgid "New user registration" |
|
|||
839 | msgstr "Рэгістрацыя новага карыстальніка" |
|
|||
840 |
|
||||
841 | msgid "" |
|
799 | msgid "" | |
842 | "You can't remove this user since it is crucial for the entire application" |
|
800 | "You can't remove this user since it is crucial for the entire application" | |
843 | msgstr "" |
|
801 | msgstr "" | |
@@ -944,13 +902,6 b' msgstr "\xd0\x93\xd1\x80\xd1\x83\xd0\xbf\xd0\xb0 \xd1\x80\xd1\x8d\xd0\xbf\xd0\xb0\xd0\xb7\xd1\x96\xd1\x82\xd0\xb0\xd1\x80\xd0\xbe\xd1\x9e \\"%(repo)s\\" \xd1\x83\xd0\xb6\xd0\xbe \xd1\x96\xd1\x81\xd0\xbd\xd1\x83\xd0\xb5"' | |||||
944 | msgid "Invalid repository URL" |
|
902 | msgid "Invalid repository URL" | |
945 | msgstr "Няслушны URL рэпазітара" |
|
903 | msgstr "Няслушны URL рэпазітара" | |
946 |
|
904 | |||
947 | msgid "" |
|
|||
948 | "Invalid repository URL. It must be a valid http, https, ssh, svn+http or " |
|
|||
949 | "svn+https URL" |
|
|||
950 | msgstr "" |
|
|||
951 | "Няслушны URL рэпазітара. Ён мусіць быць карэктным URL http, https, ssh, " |
|
|||
952 | "svn+http ці svn+https" |
|
|||
953 |
|
||||
954 | msgid "Fork has to be the same type as parent" |
|
905 | msgid "Fork has to be the same type as parent" | |
955 | msgstr "Тып форка будзе супадаць з бацькоўскім" |
|
906 | msgstr "Тып форка будзе супадаць з бацькоўскім" | |
956 |
|
907 | |||
@@ -1042,10 +993,10 b' msgstr "\xd0\x86\xd0\xbc\xd1\x8f \xd0\xba\xd0\xb0\xd1\x80\xd1\x8b\xd1\x81\xd1\x82\xd0\xb0\xd0\xbb\xd1\x8c\xd0\xbd\xd1\x96\xd0\xba\xd0\xb0"' | |||||
1042 | msgid "Password" |
|
993 | msgid "Password" | |
1043 | msgstr "Пароль" |
|
994 | msgstr "Пароль" | |
1044 |
|
995 | |||
1045 |
msgid "Forgot your password |
|
996 | msgid "Forgot your password?" | |
1046 | msgstr "Забыліся на пароль?" |
|
997 | msgstr "Забыліся на пароль?" | |
1047 |
|
998 | |||
1048 |
msgid "Don't have an account |
|
999 | msgid "Don't have an account?" | |
1049 | msgstr "Няма акаўнта?" |
|
1000 | msgstr "Няма акаўнта?" | |
1050 |
|
1001 | |||
1051 | msgid "Sign In" |
|
1002 | msgid "Sign In" | |
@@ -1627,9 +1578,6 b' msgstr "\xd0\x9f\xd1\x80\xd1\x8b\xd0\xb2\xd1\x96\xd1\x82\xd0\xb0\xd0\xbd\xd0\xbd\xd0\xb5 \xd0\xb4\xd0\xbb\xd1\x8f HTTP-\xd0\xb0\xd1\x9e\xd1\x82\xd1\x8d\xd0\xbd\xd1\x82\xd1\x8b\xd1\x84\xd1\x96\xd0\xba\xd0\xb0\xd1\x86\xd1\x8b\xd1\x96"' | |||||
1627 | msgid "Save Settings" |
|
1578 | msgid "Save Settings" | |
1628 | msgstr "Захаваць налады" |
|
1579 | msgstr "Захаваць налады" | |
1629 |
|
1580 | |||
1630 | msgid "Custom Hooks" |
|
|||
1631 | msgstr "Карыстальніцкія хукі" |
|
|||
1632 |
|
||||
1633 | msgid "Failed to remove hook" |
|
1581 | msgid "Failed to remove hook" | |
1634 | msgstr "Не атрымалася выдаліць хук" |
|
1582 | msgstr "Не атрымалася выдаліць хук" | |
1635 |
|
1583 | |||
@@ -1675,9 +1623,6 b' msgstr "\xd0\x9f\xd0\xb0\xd1\x88\xd1\x8b\xd1\x80\xd1\x8d\xd0\xbd\xd0\xbd\xd1\x96 Mercurial"' | |||||
1675 | msgid "Enable largefiles extension" |
|
1623 | msgid "Enable largefiles extension" | |
1676 | msgstr "Уключыць падтрымку вялікіх файлаў" |
|
1624 | msgstr "Уключыць падтрымку вялікіх файлаў" | |
1677 |
|
1625 | |||
1678 | msgid "Enable hgsubversion extension" |
|
|||
1679 | msgstr "Уключыць падтрымку hgsubversion" |
|
|||
1680 |
|
||||
1681 | msgid "Location of repositories" |
|
1626 | msgid "Location of repositories" | |
1682 | msgstr "Месцазнаходжанне рэпазітароў" |
|
1627 | msgstr "Месцазнаходжанне рэпазітароў" | |
1683 |
|
1628 | |||
@@ -1967,8 +1912,8 b' msgstr "\xd0\x9d\xd1\x8f\xd0\xbc\xd0\xb0 \xd1\x80\xd1\x8d\xd0\xb2\xd1\x96\xd0\xb7\xd1\x96\xd0\xb9"' | |||||
1967 | msgid "Failed to revoke permission" |
|
1912 | msgid "Failed to revoke permission" | |
1968 | msgstr "Не атрымалася адклікаць прывілеі" |
|
1913 | msgstr "Не атрымалася адклікаць прывілеі" | |
1969 |
|
1914 | |||
1970 |
msgid "Confirm to revoke permission for {0}: {1} |
|
1915 | msgid "Confirm to revoke permission for {0}: {1}?" | |
1971 |
msgstr "Пацвердзіце выдаленне прывілею для {0}: {1} |
|
1916 | msgstr "Пацвердзіце выдаленне прывілею для {0}: {1}?" | |
1972 |
|
1917 | |||
1973 | msgid "Select changeset" |
|
1918 | msgid "Select changeset" | |
1974 | msgstr "Выбраць набор змен" |
|
1919 | msgstr "Выбраць набор змен" | |
@@ -2214,6 +2159,9 b' msgstr "\xd0\x9c\xd1\x8b \xd0\xb0\xd1\x82\xd1\x80\xd1\x8b\xd0\xbc\xd0\xb0\xd0\xbb\xd1\x96 \xd0\xb7\xd0\xb0\xd0\xbf\xd1\x8b\xd1\x82 \xd0\xbd\xd0\xb0 \xd1\x81\xd0\xba\xd1\x96\xd0\xb4\xd0\xb0\xd0\xbd\xd0\xbd\xd0\xb5 \xd0\xbf\xd0\xb0\xd1\x80\xd0\xbe\xd0\xbb\xd1\x8f \xd0\xb4\xd0\xbb\xd1\x8f \xd0\xb2\xd0\xb0\xd1\x88\xd0\xb0\xd0\xb3\xd0\xb0 \xd0\xb0\xd0\xba\xd0\xb0\xd1\x9e\xd0\xbd\xd1\x82\xd0\xb0."' | |||||
2214 | msgid "File diff" |
|
2159 | msgid "File diff" | |
2215 | msgstr "Параўнанне файлаў" |
|
2160 | msgstr "Параўнанне файлаў" | |
2216 |
|
2161 | |||
|
2162 | msgid "Ignore whitespace" | |||
|
2163 | msgstr "Ігнараваць прабелы" | |||
|
2164 | ||||
2217 | msgid "%s File Diff" |
|
2165 | msgid "%s File Diff" | |
2218 | msgstr "Параўнанне файла %s" |
|
2166 | msgstr "Параўнанне файла %s" | |
2219 |
|
2167 |
@@ -10,24 +10,25 b' msgstr ""' | |||||
10 | "Content-Transfer-Encoding: 8bit\n" |
|
10 | "Content-Transfer-Encoding: 8bit\n" | |
11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" |
|
11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" | |
12 |
|
12 | |||
|
13 | msgid "" | |||
|
14 | "CSRF token leak has been detected - all form tokens have been expired" | |||
|
15 | msgstr "CSRF-token lækage opdaget, alle form-tokens er invalideret" | |||
|
16 | ||||
|
17 | msgid "Repository not found in the filesystem" | |||
|
18 | msgstr "Repository ikke fundet i filsystemet" | |||
|
19 | ||||
13 | msgid "There are no changesets yet" |
|
20 | msgid "There are no changesets yet" | |
14 | msgstr "Der er ingen changesets endnu" |
|
21 | msgstr "Der er ingen changesets endnu" | |
15 |
|
22 | |||
|
23 | msgid "Changeset for %s %s not found in %s" | |||
|
24 | msgstr "Changeset for %s %s ikke fundet i %s" | |||
|
25 | ||||
16 | msgid "None" |
|
26 | msgid "None" | |
17 | msgstr "Ingen" |
|
27 | msgstr "Ingen" | |
18 |
|
28 | |||
19 | msgid "(closed)" |
|
29 | msgid "(closed)" | |
20 | msgstr "(lukket)" |
|
30 | msgstr "(lukket)" | |
21 |
|
31 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Vis mellemrum" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Ignorer mellemrum" |
|
|||
27 |
|
||||
28 | msgid "Increase diff context to %(num)s lines" |
|
|||
29 | msgstr "Øg diff konteksten med %(num)s linjer" |
|
|||
30 |
|
||||
31 | msgid "Successfully deleted pull request %s" |
|
32 | msgid "Successfully deleted pull request %s" | |
32 | msgstr "Pull-forespørgsel %s slettet successfuldt" |
|
33 | msgstr "Pull-forespørgsel %s slettet successfuldt" | |
33 |
|
34 | |||
@@ -495,13 +496,6 b' msgstr "Der opstod en fejl under sletnin' | |||||
495 | msgid "Updated VCS settings" |
|
496 | msgid "Updated VCS settings" | |
496 | msgstr "Opdateret VCS-indstillinger" |
|
497 | msgstr "Opdateret VCS-indstillinger" | |
497 |
|
498 | |||
498 | msgid "" |
|
|||
499 | "Unable to activate hgsubversion support. The \"hgsubversion\" library is " |
|
|||
500 | "missing" |
|
|||
501 | msgstr "" |
|
|||
502 | "Ude af stand til at aktivere hgsubversion understøttelse. \"hgsubversion" |
|
|||
503 | "\" biblioteket mangler" |
|
|||
504 |
|
||||
505 | msgid "Error occurred while updating application settings" |
|
499 | msgid "Error occurred while updating application settings" | |
506 | msgstr "Der opstod en fejl ved opdatering af applikationsindstillinger" |
|
500 | msgstr "Der opstod en fejl ved opdatering af applikationsindstillinger" | |
507 |
|
501 | |||
@@ -598,16 +592,6 b' msgstr "Du skal v\xc3\xa6re registreret bruger for at kunne udf\xc3\xb8re denne handling"' | |||||
598 | msgid "You need to be signed in to view this page" |
|
592 | msgid "You need to be signed in to view this page" | |
599 | msgstr "Du skal være logget ind for at se denne side" |
|
593 | msgstr "Du skal være logget ind for at se denne side" | |
600 |
|
594 | |||
601 | msgid "" |
|
|||
602 | "CSRF token leak has been detected - all form tokens have been expired" |
|
|||
603 | msgstr "CSRF-token lækage opdaget, alle form-tokens er invalideret" |
|
|||
604 |
|
||||
605 | msgid "Repository not found in the filesystem" |
|
|||
606 | msgstr "Repository ikke fundet i filsystemet" |
|
|||
607 |
|
||||
608 | msgid "Changeset for %s %s not found in %s" |
|
|||
609 | msgstr "Changeset for %s %s ikke fundet i %s" |
|
|||
610 |
|
||||
611 | msgid "Binary file" |
|
595 | msgid "Binary file" | |
612 | msgstr "Binær fil" |
|
596 | msgstr "Binær fil" | |
613 |
|
597 | |||
@@ -620,6 +604,9 b' msgstr ""' | |||||
620 | msgid "No changes detected" |
|
604 | msgid "No changes detected" | |
621 | msgstr "Ingen ændringer fundet" |
|
605 | msgstr "Ingen ændringer fundet" | |
622 |
|
606 | |||
|
607 | msgid "Increase diff context to %(num)s lines" | |||
|
608 | msgstr "Øg diff konteksten med %(num)s linjer" | |||
|
609 | ||||
623 | msgid "Deleted branch: %s" |
|
610 | msgid "Deleted branch: %s" | |
624 | msgstr "Slettet branch: %s" |
|
611 | msgstr "Slettet branch: %s" | |
625 |
|
612 | |||
@@ -731,14 +718,6 b' msgstr "omd\xc3\xb8b"' | |||||
731 | msgid "chmod" |
|
718 | msgid "chmod" | |
732 | msgstr "chmod" |
|
719 | msgstr "chmod" | |
733 |
|
720 | |||
734 | msgid "" |
|
|||
735 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
736 | "the filesystem please run the application again in order to rescan " |
|
|||
737 | "repositories" |
|
|||
738 | msgstr "" |
|
|||
739 | "%s repository er ikke knyttet til db, måske var det skabt eller omdøbt " |
|
|||
740 | "fra filsystemet, kør applikationen igen for at scanne repositories" |
|
|||
741 |
|
||||
742 | msgid "in %s" |
|
721 | msgid "in %s" | |
743 | msgstr "i %s" |
|
722 | msgstr "i %s" | |
744 |
|
723 | |||
@@ -754,12 +733,6 b' msgstr "%s og %s siden"' | |||||
754 | msgid "just now" |
|
733 | msgid "just now" | |
755 | msgstr "lige nu" |
|
734 | msgstr "lige nu" | |
756 |
|
735 | |||
757 | msgid "on line %s" |
|
|||
758 | msgstr "på linje %s" |
|
|||
759 |
|
||||
760 | msgid "[Mention]" |
|
|||
761 | msgstr "[Omtale]" |
|
|||
762 |
|
||||
763 | msgid "top level" |
|
736 | msgid "top level" | |
764 | msgstr "top-niveau" |
|
737 | msgstr "top-niveau" | |
765 |
|
738 | |||
@@ -802,12 +775,6 b' msgstr "Standard-bruger har skrive-adgan' | |||||
802 | msgid "Default user has admin access to new user groups" |
|
775 | msgid "Default user has admin access to new user groups" | |
803 | msgstr "Standard-bruger har admin-adgang til nye brugergrupper" |
|
776 | msgstr "Standard-bruger har admin-adgang til nye brugergrupper" | |
804 |
|
777 | |||
805 | msgid "Only admins can create repository groups" |
|
|||
806 | msgstr "Kun administratorer kan oprette repository-grupper" |
|
|||
807 |
|
||||
808 | msgid "Non-admins can create repository groups" |
|
|||
809 | msgstr "Ikke-administratorer kan oprette repository-grupper" |
|
|||
810 |
|
||||
811 | msgid "Only admins can create user groups" |
|
778 | msgid "Only admins can create user groups" | |
812 | msgstr "Kun administratorer kan oprette brugergrupper" |
|
779 | msgstr "Kun administratorer kan oprette brugergrupper" | |
813 |
|
780 | |||
@@ -820,17 +787,6 b' msgstr "Kun administratorer kan oprette ' | |||||
820 | msgid "Non-admins can create top level repositories" |
|
787 | msgid "Non-admins can create top level repositories" | |
821 | msgstr "Ikke-administratorer kan oprette top-niveau repositories" |
|
788 | msgstr "Ikke-administratorer kan oprette top-niveau repositories" | |
822 |
|
789 | |||
823 | msgid "" |
|
|||
824 | "Repository creation enabled with write permission to a repository group" |
|
|||
825 | msgstr "" |
|
|||
826 | "Repository oprettelse aktiveret med skriveadgang til en repository-gruppe" |
|
|||
827 |
|
||||
828 | msgid "" |
|
|||
829 | "Repository creation disabled with write permission to a repository group" |
|
|||
830 | msgstr "" |
|
|||
831 | "Repository oprettelse deaktiveret med skriveadgang til en repository-" |
|
|||
832 | "gruppe" |
|
|||
833 |
|
||||
834 | msgid "Only admins can fork repositories" |
|
790 | msgid "Only admins can fork repositories" | |
835 | msgstr "Kun admins kan fork repositories" |
|
791 | msgstr "Kun admins kan fork repositories" | |
836 |
|
792 | |||
@@ -873,13 +829,6 b' msgstr "Indtast %(min)i tegn eller flere' | |||||
873 | msgid "Name must not contain only digits" |
|
829 | msgid "Name must not contain only digits" | |
874 | msgstr "Navn må ikke kun indeholde cifre" |
|
830 | msgstr "Navn må ikke kun indeholde cifre" | |
875 |
|
831 | |||
876 | msgid "" |
|
|||
877 | "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on " |
|
|||
878 | "%(branch)s" |
|
|||
879 | msgstr "" |
|
|||
880 | "[Kommentar] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" på " |
|
|||
881 | "%(branch)s" |
|
|||
882 |
|
||||
883 | msgid "New user %(new_username)s registered" |
|
832 | msgid "New user %(new_username)s registered" | |
884 | msgstr "Ny bruger %(new_username)s registreret" |
|
833 | msgstr "Ny bruger %(new_username)s registreret" | |
885 |
|
834 | |||
@@ -900,11 +849,8 b' msgstr ""' | |||||
900 | msgid "Closing" |
|
849 | msgid "Closing" | |
901 | msgstr "Lukning" |
|
850 | msgstr "Lukning" | |
902 |
|
851 | |||
903 | msgid "" |
|
|||
904 | "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s" |
|
|||
905 | msgstr "" |
|
|||
906 | "%(user)s vil have dig til at gennemgå pull-forespørgsel %(pr_nice_id)s: " |
|
|||
907 | "%(pr_title)s" |
|
|||
908 |
|
||||
909 | msgid "Cannot create empty pull request" |
|
852 | msgid "Cannot create empty pull request" | |
910 | msgstr "Kan ikke oprette en tom pull-forespørgsel" |
|
853 | msgstr "Kan ikke oprette en tom pull-forespørgsel" | |
|
854 | ||||
|
855 | msgid "Ignore whitespace" | |||
|
856 | msgstr "Ignorer mellemrum" |
@@ -10,6 +10,14 b' msgstr ""' | |||||
10 | "Content-Transfer-Encoding: 8bit\n" |
|
10 | "Content-Transfer-Encoding: 8bit\n" | |
11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" |
|
11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" | |
12 |
|
12 | |||
|
13 | msgid "" | |||
|
14 | "CSRF token leak has been detected - all form tokens have been expired" | |||
|
15 | msgstr "" | |||
|
16 | "Es wurde ein CSRF Leck entdeckt. Alle Formular Token sind abgelaufen" | |||
|
17 | ||||
|
18 | msgid "Repository not found in the filesystem" | |||
|
19 | msgstr "Das Repository konnte nicht im Filesystem gefunden werden" | |||
|
20 | ||||
13 | msgid "There are no changesets yet" |
|
21 | msgid "There are no changesets yet" | |
14 | msgstr "Es gibt noch keine Änderungssätze" |
|
22 | msgstr "Es gibt noch keine Änderungssätze" | |
15 |
|
23 | |||
@@ -19,15 +27,6 b' msgstr "Keine"' | |||||
19 | msgid "(closed)" |
|
27 | msgid "(closed)" | |
20 | msgstr "(geschlossen)" |
|
28 | msgstr "(geschlossen)" | |
21 |
|
29 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Zeige unsichtbare Zeichen" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Ignoriere unsichtbare Zeichen" |
|
|||
27 |
|
||||
28 | msgid "Increase diff context to %(num)s lines" |
|
|||
29 | msgstr "Erhöhe diff-Kontext auf %(num)s Zeilen" |
|
|||
30 |
|
||||
31 | msgid "Successfully deleted pull request %s" |
|
30 | msgid "Successfully deleted pull request %s" | |
32 | msgstr "Pull-Request %s erfolgreich gelöscht" |
|
31 | msgstr "Pull-Request %s erfolgreich gelöscht" | |
33 |
|
32 | |||
@@ -486,13 +485,6 b' msgstr "W\xc3\xa4hrend des l\xc3\xb6schens der Repository Statistiken trat ein Fehler auf"' | |||||
486 | msgid "Updated VCS settings" |
|
485 | msgid "Updated VCS settings" | |
487 | msgstr "VCS-Einstellungen aktualisiert" |
|
486 | msgstr "VCS-Einstellungen aktualisiert" | |
488 |
|
487 | |||
489 | msgid "" |
|
|||
490 | "Unable to activate hgsubversion support. The \"hgsubversion\" library is " |
|
|||
491 | "missing" |
|
|||
492 | msgstr "" |
|
|||
493 | "hgsubversion-Unterstützung konnte nicht aktiviert werden. Die " |
|
|||
494 | "\"hgsubversion\"-Bibliothek fehlt" |
|
|||
495 |
|
||||
496 | msgid "Error occurred while updating application settings" |
|
488 | msgid "Error occurred while updating application settings" | |
497 | msgstr "" |
|
489 | msgstr "" | |
498 | "Ein Fehler ist während der Aktualisierung der Applikationseinstellungen " |
|
490 | "Ein Fehler ist während der Aktualisierung der Applikationseinstellungen " | |
@@ -520,11 +512,6 b' msgstr "Bitte geben Sie eine E-Mail-Adre' | |||||
520 | msgid "Send email task created" |
|
512 | msgid "Send email task created" | |
521 | msgstr "Task zum Versenden von E-Mails erstellt" |
|
513 | msgstr "Task zum Versenden von E-Mails erstellt" | |
522 |
|
514 | |||
523 | msgid "Builtin hooks are read-only. Please use another hook name." |
|
|||
524 | msgstr "" |
|
|||
525 | "Die eingebauten Hooks sind schreibgeschützt. Bitte verwenden Sie einen " |
|
|||
526 | "anderen Hook-Namen." |
|
|||
527 |
|
||||
528 | msgid "Added new hook" |
|
515 | msgid "Added new hook" | |
529 | msgstr "Neuer Hook hinzugefügt" |
|
516 | msgstr "Neuer Hook hinzugefügt" | |
530 |
|
517 | |||
@@ -604,14 +591,6 b' msgstr ""' | |||||
604 | msgid "You need to be signed in to view this page" |
|
591 | msgid "You need to be signed in to view this page" | |
605 | msgstr "Sie müssen sich anmelden um diese Seite aufzurufen" |
|
592 | msgstr "Sie müssen sich anmelden um diese Seite aufzurufen" | |
606 |
|
593 | |||
607 | msgid "" |
|
|||
608 | "CSRF token leak has been detected - all form tokens have been expired" |
|
|||
609 | msgstr "" |
|
|||
610 | "Es wurde ein CSRF Leck entdeckt. Alle Formular Token sind abgelaufen" |
|
|||
611 |
|
||||
612 | msgid "Repository not found in the filesystem" |
|
|||
613 | msgstr "Das Repository konnte nicht im Filesystem gefunden werden" |
|
|||
614 |
|
||||
615 | msgid "Binary file" |
|
594 | msgid "Binary file" | |
616 | msgstr "Binäre Datei" |
|
595 | msgstr "Binäre Datei" | |
617 |
|
596 | |||
@@ -624,6 +603,9 b' msgstr ""' | |||||
624 | msgid "No changes detected" |
|
603 | msgid "No changes detected" | |
625 | msgstr "Keine Änderungen erkannt" |
|
604 | msgstr "Keine Änderungen erkannt" | |
626 |
|
605 | |||
|
606 | msgid "Increase diff context to %(num)s lines" | |||
|
607 | msgstr "Erhöhe diff-Kontext auf %(num)s Zeilen" | |||
|
608 | ||||
627 | msgid "Deleted branch: %s" |
|
609 | msgid "Deleted branch: %s" | |
628 | msgstr "Branch %s gelöscht" |
|
610 | msgstr "Branch %s gelöscht" | |
629 |
|
611 | |||
@@ -732,15 +714,6 b' msgstr "umbenennen"' | |||||
732 | msgid "chmod" |
|
714 | msgid "chmod" | |
733 | msgstr "chmod" |
|
715 | msgstr "chmod" | |
734 |
|
716 | |||
735 | msgid "" |
|
|||
736 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
737 | "the filesystem please run the application again in order to rescan " |
|
|||
738 | "repositories" |
|
|||
739 | msgstr "" |
|
|||
740 | "Das %s Repository ist nicht in der Datenbank vorhanden, eventuell wurde " |
|
|||
741 | "es im Dateisystem erstellt oder umbenannt. Bitte starten sie die " |
|
|||
742 | "Applikation erneut um die Repositories neu zu Indizieren" |
|
|||
743 |
|
||||
744 | msgid "%d year" |
|
717 | msgid "%d year" | |
745 | msgid_plural "%d years" |
|
718 | msgid_plural "%d years" | |
746 | msgstr[0] "%d Jahr" |
|
719 | msgstr[0] "%d Jahr" | |
@@ -786,12 +759,6 b' msgstr "%s und %s her"' | |||||
786 | msgid "just now" |
|
759 | msgid "just now" | |
787 | msgstr "jetzt gerade" |
|
760 | msgstr "jetzt gerade" | |
788 |
|
761 | |||
789 | msgid "on line %s" |
|
|||
790 | msgstr "in Zeile %s" |
|
|||
791 |
|
||||
792 | msgid "[Mention]" |
|
|||
793 | msgstr "[Mention]" |
|
|||
794 |
|
||||
795 | msgid "top level" |
|
762 | msgid "top level" | |
796 | msgstr "höchste Ebene" |
|
763 | msgstr "höchste Ebene" | |
797 |
|
764 | |||
@@ -835,12 +802,6 b' msgstr "Der Standard-Benutzer hat Schrei' | |||||
835 | msgid "Default user has admin access to new user groups" |
|
802 | msgid "Default user has admin access to new user groups" | |
836 | msgstr "Der Standard-Benutzer hat Admin-Rechte auf neuen Benutzer-Gruppen" |
|
803 | msgstr "Der Standard-Benutzer hat Admin-Rechte auf neuen Benutzer-Gruppen" | |
837 |
|
804 | |||
838 | msgid "Only admins can create repository groups" |
|
|||
839 | msgstr "Nur Admins können Repository-Gruppen erstellen" |
|
|||
840 |
|
||||
841 | msgid "Non-admins can create repository groups" |
|
|||
842 | msgstr "Nicht-Admins können Repository-Gruppen erstellen" |
|
|||
843 |
|
||||
844 | msgid "Only admins can create user groups" |
|
805 | msgid "Only admins can create user groups" | |
845 | msgstr "Nur Admins können Benutzer-Gruppen erstellen" |
|
806 | msgstr "Nur Admins können Benutzer-Gruppen erstellen" | |
846 |
|
807 | |||
@@ -853,18 +814,6 b' msgstr "Nur Admins k\xc3\xb6nnen Repositories auf oberster Ebene erstellen"' | |||||
853 | msgid "Non-admins can create top level repositories" |
|
814 | msgid "Non-admins can create top level repositories" | |
854 | msgstr "Nicht-Admins können Repositories oberster Ebene erstellen" |
|
815 | msgstr "Nicht-Admins können Repositories oberster Ebene erstellen" | |
855 |
|
816 | |||
856 | msgid "" |
|
|||
857 | "Repository creation enabled with write permission to a repository group" |
|
|||
858 | msgstr "" |
|
|||
859 | "Erstellung von Repositories mit Schreibzugriff für Repositorygruppe " |
|
|||
860 | "aktiviert" |
|
|||
861 |
|
||||
862 | msgid "" |
|
|||
863 | "Repository creation disabled with write permission to a repository group" |
|
|||
864 | msgstr "" |
|
|||
865 | "Erstellung von Repositories mit Schreibzugriff für Repositorygruppe " |
|
|||
866 | "deaktiviert" |
|
|||
867 |
|
||||
868 | msgid "Only admins can fork repositories" |
|
817 | msgid "Only admins can fork repositories" | |
869 | msgstr "Nur Admins können Repositories forken" |
|
818 | msgstr "Nur Admins können Repositories forken" | |
870 |
|
819 | |||
@@ -926,9 +875,6 b' msgstr "Geschlossen, n\xc3\xa4chste Iteration: %s ."' | |||||
926 | msgid "latest tip" |
|
875 | msgid "latest tip" | |
927 | msgstr "Letzter Tip" |
|
876 | msgstr "Letzter Tip" | |
928 |
|
877 | |||
929 | msgid "New user registration" |
|
|||
930 | msgstr "Neue Benutzerregistrierung" |
|
|||
931 |
|
||||
932 | msgid "" |
|
878 | msgid "" | |
933 | "User \"%s\" still owns %s repositories and cannot be removed. Switch " |
|
879 | "User \"%s\" still owns %s repositories and cannot be removed. Switch " | |
934 | "owners or remove those repositories: %s" |
|
880 | "owners or remove those repositories: %s" | |
@@ -1021,13 +967,6 b' msgstr "Eine Repositorygruppe mit dem Na' | |||||
1021 | msgid "Invalid repository URL" |
|
967 | msgid "Invalid repository URL" | |
1022 | msgstr "Ungültige Repository-URL" |
|
968 | msgstr "Ungültige Repository-URL" | |
1023 |
|
969 | |||
1024 | msgid "" |
|
|||
1025 | "Invalid repository URL. It must be a valid http, https, ssh, svn+http or " |
|
|||
1026 | "svn+https URL" |
|
|||
1027 | msgstr "" |
|
|||
1028 | "Ungültige Repository-URL. Es muss eine gültige http, https, ssh, svn+http " |
|
|||
1029 | "oder svn+https URL sein" |
|
|||
1030 |
|
||||
1031 | msgid "Fork has to be the same type as parent" |
|
970 | msgid "Fork has to be the same type as parent" | |
1032 | msgstr "Forke um den selben typ wie der Vorgesetze zu haben" |
|
971 | msgstr "Forke um den selben typ wie der Vorgesetze zu haben" | |
1033 |
|
972 | |||
@@ -1129,10 +1068,10 b' msgstr "Passwort"' | |||||
1129 | msgid "Stay logged in after browser restart" |
|
1068 | msgid "Stay logged in after browser restart" | |
1130 | msgstr "Nach dem Neustart des Browsers eingeloggt bleiben" |
|
1069 | msgstr "Nach dem Neustart des Browsers eingeloggt bleiben" | |
1131 |
|
1070 | |||
1132 |
msgid "Forgot your password |
|
1071 | msgid "Forgot your password?" | |
1133 | msgstr "Passwort vergessen?" |
|
1072 | msgstr "Passwort vergessen?" | |
1134 |
|
1073 | |||
1135 |
msgid "Don't have an account |
|
1074 | msgid "Don't have an account?" | |
1136 | msgstr "Kein Account?" |
|
1075 | msgstr "Kein Account?" | |
1137 |
|
1076 | |||
1138 | msgid "Sign In" |
|
1077 | msgid "Sign In" | |
@@ -1566,25 +1505,6 b' msgstr ""' | |||||
1566 | "Aktiviere dies, damit Nicht-Administratoren Repositories auf der obersten " |
|
1505 | "Aktiviere dies, damit Nicht-Administratoren Repositories auf der obersten " | |
1567 | "Ebene erstellen können." |
|
1506 | "Ebene erstellen können." | |
1568 |
|
1507 | |||
1569 | msgid "" |
|
|||
1570 | "Note: This will also give all users API access to create repositories " |
|
|||
1571 | "everywhere. That might change in future versions." |
|
|||
1572 | msgstr "" |
|
|||
1573 | "Hinweis: dadurch erhalten auch alle Benutzer API-Zugriff, um überall " |
|
|||
1574 | "Repositories zu erstellen. Das kann sich in zukünftigen Versionen ändern." |
|
|||
1575 |
|
||||
1576 | msgid "Repository creation with group write access" |
|
|||
1577 | msgstr "Repository-Erstellung mit Gruppen-Schreibzugriff" |
|
|||
1578 |
|
||||
1579 | msgid "" |
|
|||
1580 | "With this, write permission to a repository group allows creating " |
|
|||
1581 | "repositories inside that group. Without this, group write permissions " |
|
|||
1582 | "mean nothing." |
|
|||
1583 | msgstr "" |
|
|||
1584 | "Falls aktiv, gewährt dies das Recht zum Erzeugen von Repositories in " |
|
|||
1585 | "einer Repository-Gruppe. Falls inaktiv, sind Gruppen-" |
|
|||
1586 | "Schreibberechtigungen wirkungslos." |
|
|||
1587 |
|
||||
1588 | msgid "User group creation" |
|
1508 | msgid "User group creation" | |
1589 | msgstr "Benutzergruppen Erstellung" |
|
1509 | msgstr "Benutzergruppen Erstellung" | |
1590 |
|
1510 | |||
@@ -1988,12 +1908,6 b' msgstr ""' | |||||
1988 | msgid "Save Settings" |
|
1908 | msgid "Save Settings" | |
1989 | msgstr "Einstellungen speichern" |
|
1909 | msgstr "Einstellungen speichern" | |
1990 |
|
1910 | |||
1991 | msgid "Built-in Mercurial Hooks (Read-Only)" |
|
|||
1992 | msgstr "Eingebaute Mercurial Hooks (Read -Only)" |
|
|||
1993 |
|
||||
1994 | msgid "Custom Hooks" |
|
|||
1995 | msgstr "Benutzerdefinierte Hooks" |
|
|||
1996 |
|
||||
1997 | msgid "" |
|
1911 | msgid "" | |
1998 | "Hooks can be used to trigger actions on certain events such as push / " |
|
1912 | "Hooks can be used to trigger actions on certain events such as push / " | |
1999 | "pull. They can trigger Python functions or external applications." |
|
1913 | "pull. They can trigger Python functions or external applications." | |
@@ -2027,27 +1941,6 b' msgstr ""' | |||||
2027 | msgid "Install Git hooks" |
|
1941 | msgid "Install Git hooks" | |
2028 | msgstr "Git-Hooks installieren" |
|
1942 | msgstr "Git-Hooks installieren" | |
2029 |
|
1943 | |||
2030 | msgid "" |
|
|||
2031 | "Verify if Kallithea's Git hooks are installed for each repository. " |
|
|||
2032 | "Current hooks will be updated to the latest version." |
|
|||
2033 | msgstr "" |
|
|||
2034 | "Überprüfen Sie, ob die Git-Hooks von Kallithea für jedes Repository " |
|
|||
2035 | "installiert sind. Aktuelle Hooks werden auf die neueste Version " |
|
|||
2036 | "aktualisiert." |
|
|||
2037 |
|
||||
2038 | msgid "Overwrite existing Git hooks" |
|
|||
2039 | msgstr "Bestehende Git-Hooks überschreiben" |
|
|||
2040 |
|
||||
2041 | msgid "" |
|
|||
2042 | "If installing Git hooks, overwrite any existing hooks, even if they do " |
|
|||
2043 | "not seem to come from Kallithea. WARNING: This operation will destroy any " |
|
|||
2044 | "custom git hooks you may have deployed by hand!" |
|
|||
2045 | msgstr "" |
|
|||
2046 | "Wenn Sie Git-Hooks installieren, überschreiben Sie alle vorhandenen " |
|
|||
2047 | "Hooks, auch wenn sie nicht von Kallithea zu kommen scheinen. WARNUNG: " |
|
|||
2048 | "Diese Operation zerstört alle benutzerdefinierten Git-Hooks, die Sie " |
|
|||
2049 | "möglicherweise von Hand bereitgestellt haben!" |
|
|||
2050 |
|
||||
2051 | msgid "Rescan Repositories" |
|
1944 | msgid "Rescan Repositories" | |
2052 | msgstr "Repositories erneut scannen" |
|
1945 | msgstr "Repositories erneut scannen" | |
2053 |
|
1946 | |||
@@ -2103,17 +1996,6 b' msgstr "Mercurial-Erweiterungen"' | |||||
2103 | msgid "Enable largefiles extension" |
|
1996 | msgid "Enable largefiles extension" | |
2104 | msgstr "Erweiterung largefiles aktivieren" |
|
1997 | msgstr "Erweiterung largefiles aktivieren" | |
2105 |
|
1998 | |||
2106 | msgid "Enable hgsubversion extension" |
|
|||
2107 | msgstr "Erweiterung hgsubversion aktivieren" |
|
|||
2108 |
|
||||
2109 | msgid "" |
|
|||
2110 | "Requires hgsubversion library to be installed. Enables cloning of remote " |
|
|||
2111 | "Subversion repositories while converting them to Mercurial." |
|
|||
2112 | msgstr "" |
|
|||
2113 | "Erfordert die Installation der hgsubversion-Bibliothek. Ermöglicht das " |
|
|||
2114 | "Klonen von entfernten Subversion-Repositories während der Konvertierung " |
|
|||
2115 | "zu Mercurial." |
|
|||
2116 |
|
||||
2117 | msgid "Location of repositories" |
|
1999 | msgid "Location of repositories" | |
2118 | msgstr "Ort der Repositories" |
|
2000 | msgstr "Ort der Repositories" | |
2119 |
|
2001 | |||
@@ -2351,7 +2233,7 b' msgstr "Einen weiteren Kommentar hinzuf\xc3\xbcgen"' | |||||
2351 | msgid "Group" |
|
2233 | msgid "Group" | |
2352 | msgstr "Gruppe" |
|
2234 | msgstr "Gruppe" | |
2353 |
|
2235 | |||
2354 |
msgid "Confirm to revoke permission for {0}: {1} |
|
2236 | msgid "Confirm to revoke permission for {0}: {1}?" | |
2355 | msgstr "Widerruf der Rechte für {0}: {1} bestätigen?" |
|
2237 | msgstr "Widerruf der Rechte für {0}: {1} bestätigen?" | |
2356 |
|
2238 | |||
2357 | msgid "Select changeset" |
|
2239 | msgid "Select changeset" | |
@@ -2441,6 +2323,9 b' msgstr "Abonniere den %s ATOM Feed"' | |||||
2441 | msgid "Hello %s" |
|
2323 | msgid "Hello %s" | |
2442 | msgstr "Hallo %s" |
|
2324 | msgstr "Hallo %s" | |
2443 |
|
2325 | |||
|
2326 | msgid "Ignore whitespace" | |||
|
2327 | msgstr "Ignoriere unsichtbare Zeichen" | |||
|
2328 | ||||
2444 | msgid "or" |
|
2329 | msgid "or" | |
2445 | msgstr "oder" |
|
2330 | msgstr "oder" | |
2446 |
|
2331 |
@@ -10,24 +10,30 b' msgstr ""' | |||||
10 | "Content-Transfer-Encoding: 8bit\n" |
|
10 | "Content-Transfer-Encoding: 8bit\n" | |
11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" |
|
11 | "Plural-Forms: nplurals=2; plural=n != 1;\n" | |
12 |
|
12 | |||
|
13 | msgid "" | |||
|
14 | "CSRF token leak has been detected - all form tokens have been expired" | |||
|
15 | msgstr "" | |||
|
16 | "Εντοπίστηκε διαρροή ενός διακριτικού CSRF - όλα τα διακριτικά της φόρμας " | |||
|
17 | "έχουν λήξει" | |||
|
18 | ||||
|
19 | msgid "Repository not found in the filesystem" | |||
|
20 | msgstr "Το αποθετήριο δε βρέθηκε στο σύστημα αρχείων" | |||
|
21 | ||||
13 | msgid "There are no changesets yet" |
|
22 | msgid "There are no changesets yet" | |
14 | msgstr "Δεν υπάρχουν σετ αλλαγών ακόμα" |
|
23 | msgstr "Δεν υπάρχουν σετ αλλαγών ακόμα" | |
15 |
|
24 | |||
|
25 | msgid "Changeset for %s %s not found in %s" | |||
|
26 | msgstr "Το σετ αλλαγών για %s %sδεν βρέθηκε στο %s" | |||
|
27 | ||||
|
28 | msgid "SSH access is disabled." | |||
|
29 | msgstr "Η πρόσβαση μέσω SSH είναι απενεργοποιημένη." | |||
|
30 | ||||
16 | msgid "None" |
|
31 | msgid "None" | |
17 | msgstr "Χωρίς" |
|
32 | msgstr "Χωρίς" | |
18 |
|
33 | |||
19 | msgid "(closed)" |
|
34 | msgid "(closed)" | |
20 | msgstr "(κλειστό)" |
|
35 | msgstr "(κλειστό)" | |
21 |
|
36 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Εμφάνιση κενού" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Αγνόηση κενού" |
|
|||
27 |
|
||||
28 | msgid "Increase diff context to %(num)s lines" |
|
|||
29 | msgstr "Αύξηση του diff πλαισίου σε %(num)s γραμμές" |
|
|||
30 |
|
||||
31 | msgid "No permission to change status" |
|
37 | msgid "No permission to change status" | |
32 | msgstr "Χωρίς δικαιώματα αλλαγής της κατάστασης" |
|
38 | msgstr "Χωρίς δικαιώματα αλλαγής της κατάστασης" | |
33 |
|
39 | |||
@@ -543,13 +549,6 b' msgstr ""' | |||||
543 | msgid "Updated VCS settings" |
|
549 | msgid "Updated VCS settings" | |
544 | msgstr "Ενημερωμένες ρυθμίσεις VCS" |
|
550 | msgstr "Ενημερωμένες ρυθμίσεις VCS" | |
545 |
|
551 | |||
546 | msgid "" |
|
|||
547 | "Unable to activate hgsubversion support. The \"hgsubversion\" library is " |
|
|||
548 | "missing" |
|
|||
549 | msgstr "" |
|
|||
550 | "Δεν γίνεται να ενεργοποιηθεί υποστήριξη για το hgsubversion. Λείπει η " |
|
|||
551 | "βιβλιοθήκη \"hgsubversion\"" |
|
|||
552 |
|
||||
553 | msgid "Error occurred while updating application settings" |
|
552 | msgid "Error occurred while updating application settings" | |
554 | msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση των ρυθμίσεων της εφαρμογής" |
|
553 | msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση των ρυθμίσεων της εφαρμογής" | |
555 |
|
554 | |||
@@ -578,11 +577,6 b' msgstr "\xce\x94\xce\xb7\xce\xbc\xce\xb9\xce\xbf\xcf\x85\xcf\x81\xce\xb3\xce\xae\xce\xb8\xce\xb7\xce\xba\xce\xb5 \xce\xb7 \xce\xb5\xcf\x81\xce\xb3\xce\xb1\xcf\x83\xce\xaf\xce\xb1 \xcf\x84\xce\xb7\xcf\x82 \xce\xb1\xcf\x80\xce\xbf\xcf\x83\xcf\x84\xce\xbf\xce\xbb\xce\xae\xcf\x82 \xce\xb7\xce\xbb\xce\xb5\xce\xba\xcf\x84\xcf\x81\xce\xbf\xce\xbd\xce\xb9\xce\xba\xce\xbf\xcf\x8d \xcf\x84\xce\xb1\xcf\x87\xcf\x85\xce\xb4\xcf\x81\xce\xbf\xce\xbc\xce\xb5\xce\xaf\xce\xbf\xcf\x85"' | |||||
578 | msgid "Hook already exists" |
|
577 | msgid "Hook already exists" | |
579 | msgstr "Το άγκιστρο υπάρχει ήδη" |
|
578 | msgstr "Το άγκιστρο υπάρχει ήδη" | |
580 |
|
579 | |||
581 | msgid "Builtin hooks are read-only. Please use another hook name." |
|
|||
582 | msgstr "" |
|
|||
583 | "Τα ενσωματωμένα άγκιστρα είναι μόνο για ανάγνωση. Παρακαλώ δώστε άλλο " |
|
|||
584 | "όνομα στο άγκιστρο." |
|
|||
585 |
|
||||
586 | msgid "Added new hook" |
|
580 | msgid "Added new hook" | |
587 | msgstr "Προσθήκη νέου άγκιστρου" |
|
581 | msgstr "Προσθήκη νέου άγκιστρου" | |
588 |
|
582 | |||
@@ -659,21 +653,6 b' msgstr ""' | |||||
659 | msgid "You need to be signed in to view this page" |
|
653 | msgid "You need to be signed in to view this page" | |
660 | msgstr "Πρέπει να είστε συνδεμένος για να δείτε αυτήν τη σελίδα" |
|
654 | msgstr "Πρέπει να είστε συνδεμένος για να δείτε αυτήν τη σελίδα" | |
661 |
|
655 | |||
662 | msgid "" |
|
|||
663 | "CSRF token leak has been detected - all form tokens have been expired" |
|
|||
664 | msgstr "" |
|
|||
665 | "Εντοπίστηκε διαρροή ενός διακριτικού CSRF - όλα τα διακριτικά της φόρμας " |
|
|||
666 | "έχουν λήξει" |
|
|||
667 |
|
||||
668 | msgid "Repository not found in the filesystem" |
|
|||
669 | msgstr "Το αποθετήριο δε βρέθηκε στο σύστημα αρχείων" |
|
|||
670 |
|
||||
671 | msgid "Changeset for %s %s not found in %s" |
|
|||
672 | msgstr "Το σετ αλλαγών για %s %sδεν βρέθηκε στο %s" |
|
|||
673 |
|
||||
674 | msgid "SSH access is disabled." |
|
|||
675 | msgstr "Η πρόσβαση μέσω SSH είναι απενεργοποιημένη." |
|
|||
676 |
|
||||
677 | msgid "Binary file" |
|
656 | msgid "Binary file" | |
678 | msgstr "Δυαδικό αρχείο" |
|
657 | msgstr "Δυαδικό αρχείο" | |
679 |
|
658 | |||
@@ -686,6 +665,9 b' msgstr ""' | |||||
686 | msgid "No changes detected" |
|
665 | msgid "No changes detected" | |
687 | msgstr "Δεν εντοπίστηκαν αλλαγές" |
|
666 | msgstr "Δεν εντοπίστηκαν αλλαγές" | |
688 |
|
667 | |||
|
668 | msgid "Increase diff context to %(num)s lines" | |||
|
669 | msgstr "Αύξηση του diff πλαισίου σε %(num)s γραμμές" | |||
|
670 | ||||
689 | msgid "Deleted branch: %s" |
|
671 | msgid "Deleted branch: %s" | |
690 | msgstr "Διαγραφή κλάδου: %s" |
|
672 | msgstr "Διαγραφή κλάδου: %s" | |
691 |
|
673 | |||
@@ -797,40 +779,9 b' msgstr "\xce\xbc\xce\xb5\xcf\x84\xce\xbf\xce\xbd\xce\xbf\xce\xbc\xce\xb1\xcf\x83\xce\xaf\xce\xb1"' | |||||
797 | msgid "chmod" |
|
779 | msgid "chmod" | |
798 | msgstr "chmod" |
|
780 | msgstr "chmod" | |
799 |
|
781 | |||
800 | msgid "" |
|
|||
801 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
802 | "the filesystem please run the application again in order to rescan " |
|
|||
803 | "repositories" |
|
|||
804 | msgstr "" |
|
|||
805 | "Το αποθετήριο δεδομένων %s δεν έχει αντιστοιχιστεί στη βάση δεδομένων. " |
|
|||
806 | "Ίσως δημιουργήθηκε ή μετονομάστηκε από το σύστημα αρχείων. Εκτελέστε ξανά " |
|
|||
807 | "την εφαρμογή για να σαρώσετε ξανά τα αποθετήρια δεδομένων" |
|
|||
808 |
|
||||
809 | msgid "SSH key is missing" |
|
782 | msgid "SSH key is missing" | |
810 | msgstr "Το κλειδί SSH λείπει" |
|
783 | msgstr "Το κλειδί SSH λείπει" | |
811 |
|
784 | |||
812 | msgid "" |
|
|||
813 | "Incorrect SSH key - it must have both a key type and a base64 part, like " |
|
|||
814 | "'ssh-rsa ASRNeaZu4FA...xlJp='" |
|
|||
815 | msgstr "" |
|
|||
816 | "Λανθασμένο κλειδί SSH - πρέπει να έχει έναν τύπο κλειδιού καθώς και ένα " |
|
|||
817 | "τμήμα base64, όπως \"ssh-rsa ASRNeaZu4FA ... xlJp =\"" |
|
|||
818 |
|
||||
819 | msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'" |
|
|||
820 | msgstr "Εσφαλμένο κλειδί SSH - πρέπει να ξεκινά με 'ssh-(rsa|dss|ed25519)'" |
|
|||
821 |
|
||||
822 | msgid "Incorrect SSH key - unexpected characters in base64 part %r" |
|
|||
823 | msgstr "" |
|
|||
824 | "Εσφαλμένο κλειδί SSH - μη αναμενόμενοι χαρακτήρες στο τμήμα base64 %r" |
|
|||
825 |
|
||||
826 | msgid "Incorrect SSH key - failed to decode base64 part %r" |
|
|||
827 | msgstr "" |
|
|||
828 | "Εσφαλμένο κλειδί SSH - απέτυχε η αποκωδικοποίηση του τμήματος base64 %r" |
|
|||
829 |
|
||||
830 | msgid "Incorrect SSH key - base64 part is not %r as claimed but %r" |
|
|||
831 | msgstr "" |
|
|||
832 | "Εσφαλμένο κλειδί SSH - το base64 μέρος δεν είναι %r όπως ζητήθηκε, αλλά %r" |
|
|||
833 |
|
||||
834 | msgid "%d year" |
|
785 | msgid "%d year" | |
835 | msgid_plural "%d years" |
|
786 | msgid_plural "%d years" | |
836 | msgstr[0] "%d έτος" |
|
787 | msgstr[0] "%d έτος" | |
@@ -876,12 +827,6 b' msgstr "%s \xce\xba\xce\xb1\xce\xb9 %s \xcf\x80\xcf\x81\xce\xb9\xce\xbd"' | |||||
876 | msgid "just now" |
|
827 | msgid "just now" | |
877 | msgstr "μόλις τώρα" |
|
828 | msgstr "μόλις τώρα" | |
878 |
|
829 | |||
879 | msgid "on line %s" |
|
|||
880 | msgstr "στη γραμμή %s" |
|
|||
881 |
|
||||
882 | msgid "[Mention]" |
|
|||
883 | msgstr "[Αναφορά]" |
|
|||
884 |
|
||||
885 | msgid "top level" |
|
830 | msgid "top level" | |
886 | msgstr "ανώτερο επίπεδο" |
|
831 | msgstr "ανώτερο επίπεδο" | |
887 |
|
832 | |||
@@ -934,12 +879,6 b' msgid "Default user has admin access to ' | |||||
934 | msgstr "" |
|
879 | msgstr "" | |
935 | "Ο προεπιλεγμένος χρήστης έχει πρόσβαση διαχειριστή σε νέες ομάδες χρηστών" |
|
880 | "Ο προεπιλεγμένος χρήστης έχει πρόσβαση διαχειριστή σε νέες ομάδες χρηστών" | |
936 |
|
881 | |||
937 | msgid "Only admins can create repository groups" |
|
|||
938 | msgstr "Μόνο οι διαχειριστές μπορούν να δημιουργήσουν ομάδες αποθετηρίων" |
|
|||
939 |
|
||||
940 | msgid "Non-admins can create repository groups" |
|
|||
941 | msgstr "Οι μη διαχειριστές μπορούν να δημιουργήσουν ομάδες αποθετηρίων" |
|
|||
942 |
|
||||
943 | msgid "Only admins can create user groups" |
|
882 | msgid "Only admins can create user groups" | |
944 | msgstr "Μόνο οι διαχειριστές μπορούν να δημιουργήσουν ομάδες χρηστών" |
|
883 | msgstr "Μόνο οι διαχειριστές μπορούν να δημιουργήσουν ομάδες χρηστών" | |
945 |
|
884 | |||
@@ -954,18 +893,6 b' msgid "Non-admins can create top level r' | |||||
954 | msgstr "" |
|
893 | msgstr "" | |
955 | "Οι μη διαχειριστές μπορούν να δημιουργήσουν αποθετήρια ανώτατου επιπέδου" |
|
894 | "Οι μη διαχειριστές μπορούν να δημιουργήσουν αποθετήρια ανώτατου επιπέδου" | |
956 |
|
895 | |||
957 | msgid "" |
|
|||
958 | "Repository creation enabled with write permission to a repository group" |
|
|||
959 | msgstr "" |
|
|||
960 | "Η δημιουργία αποθετηρίου είναι ενεργοποιημένη με δικαιώματα εγγραφής σε " |
|
|||
961 | "μια ομάδα αποθετηρίων" |
|
|||
962 |
|
||||
963 | msgid "" |
|
|||
964 | "Repository creation disabled with write permission to a repository group" |
|
|||
965 | msgstr "" |
|
|||
966 | "Η δημιουργία αποθετηρίου απενεργοποιήθηκε με δικαιώματα εγγραφής σε μια " |
|
|||
967 | "ομάδα αποθετηρίων" |
|
|||
968 |
|
||||
969 | msgid "Only admins can fork repositories" |
|
896 | msgid "Only admins can fork repositories" | |
970 | msgstr "Μόνο οι διαχειριστές μπορούν να κλωνοποιήσουν τα αποθετήρια" |
|
897 | msgstr "Μόνο οι διαχειριστές μπορούν να κλωνοποιήσουν τα αποθετήρια" | |
971 |
|
898 | |||
@@ -1014,12 +941,6 b' msgstr "\xce\x9a\xce\xb1\xcf\x84\xce\xb1\xcf\x87\xcf\x89\xcf\x81\xce\xae\xce\xb8\xce\xb7\xce\xba\xce\xb5 \xce\xbd\xce\xad\xce\xbf\xcf\x82 \xcf\x87\xcf\x81\xce\xae\xcf\x83\xcf\x84\xce\xb7\xcf\x82 %(new_username)s"' | |||||
1014 | msgid "Closing" |
|
941 | msgid "Closing" | |
1015 | msgstr "Κλείσιμο" |
|
942 | msgstr "Κλείσιμο" | |
1016 |
|
943 | |||
1017 | msgid "" |
|
|||
1018 | "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s" |
|
|||
1019 | msgstr "" |
|
|||
1020 | "Ο χρήστης %(user)s θέλει να αναθεωρήσετε την αίτηση έλξης %(pr_nice_id)s: " |
|
|||
1021 | "%(pr_title)s" |
|
|||
1022 |
|
||||
1023 | msgid "Cannot create empty pull request" |
|
944 | msgid "Cannot create empty pull request" | |
1024 | msgstr "Δεν είναι δυνατή η δημιουργία κενής αίτησης έλξης" |
|
945 | msgstr "Δεν είναι δυνατή η δημιουργία κενής αίτησης έλξης" | |
1025 |
|
946 | |||
@@ -1067,9 +988,6 b' msgstr "\xce\xa4\xce\xbf \xce\xba\xce\xbb\xce\xb5\xce\xb9\xce\xb4\xce\xaf SSH %s \xcf\x87\xcf\x81\xce\xb7\xcf\x83\xce\xb9\xce\xbc\xce\xbf\xcf\x80\xce\xbf\xce\xb9\xce\xb5\xce\xaf\xcf\x84\xce\xb1\xce\xb9 \xce\xae\xce\xb4\xce\xb7 \xce\xb1\xcf\x80\xcf\x8c \xcf\x84\xce\xbf \xcf\x87\xcf\x81\xce\xae\xcf\x83\xcf\x84\xce\xb7 %s"' | |||||
1067 | msgid "SSH key with fingerprint %r found" |
|
988 | msgid "SSH key with fingerprint %r found" | |
1068 | msgstr "Βρέθηκε κλειδί SSH με δακτυλικό αποτύπωμα %r" |
|
989 | msgstr "Βρέθηκε κλειδί SSH με δακτυλικό αποτύπωμα %r" | |
1069 |
|
990 | |||
1070 | msgid "New user registration" |
|
|||
1071 | msgstr "Εγγραφή νέου χρήστη" |
|
|||
1072 |
|
||||
1073 | msgid "" |
|
991 | msgid "" | |
1074 | "You can't remove this user since it is crucial for the entire application" |
|
992 | "You can't remove this user since it is crucial for the entire application" | |
1075 | msgstr "" |
|
993 | msgstr "" | |
@@ -1185,13 +1103,6 b' msgstr "\xce\x97 \xce\xbf\xce\xbc\xce\xac\xce\xb4\xce\xb1 \xce\xb1\xcf\x80\xce\xbf\xce\xb8\xce\xb5\xcf\x84\xce\xb7\xcf\x81\xce\xaf\xce\xbf\xcf\x85 \xce\xbc\xce\xb5 \xcf\x84\xce\xbf \xcf\x8c\xce\xbd\xce\xbf\xce\xbc\xce\xb1 \\"%(repo)s\\" \xcf\x85\xcf\x80\xce\xac\xcf\x81\xcf\x87\xce\xb5\xce\xb9 \xce\xae\xce\xb4\xce\xb7"' | |||||
1185 | msgid "Invalid repository URL" |
|
1103 | msgid "Invalid repository URL" | |
1186 | msgstr "Μη έγκυρη διεύθυνση URL αποθετηρίου" |
|
1104 | msgstr "Μη έγκυρη διεύθυνση URL αποθετηρίου" | |
1187 |
|
1105 | |||
1188 | msgid "" |
|
|||
1189 | "Invalid repository URL. It must be a valid http, https, ssh, svn+http or " |
|
|||
1190 | "svn+https URL" |
|
|||
1191 | msgstr "" |
|
|||
1192 | "Μη έγκυρη διεύθυνση URL του αποθετηρίου. Πρέπει να είναι μια έγκυρη http, " |
|
|||
1193 | "https, ssh, svn+http ή svn+https διεύθυνση URL" |
|
|||
1194 |
|
||||
1195 | msgid "Fork has to be the same type as parent" |
|
1106 | msgid "Fork has to be the same type as parent" | |
1196 | msgstr "Ο κλώνος πρέπει να έχει τον ίδιο τύπο με τον γονέα του" |
|
1107 | msgstr "Ο κλώνος πρέπει να έχει τον ίδιο τύπο με τον γονέα του" | |
1197 |
|
1108 | |||
@@ -1293,10 +1204,10 b' msgid "Stay logged in after browser rest' | |||||
1293 | msgstr "" |
|
1204 | msgstr "" | |
1294 | "Μείνετε συνδεδεμένοι μετά την επανεκκίνηση του προγράμματος περιήγησης" |
|
1205 | "Μείνετε συνδεδεμένοι μετά την επανεκκίνηση του προγράμματος περιήγησης" | |
1295 |
|
1206 | |||
1296 |
msgid "Forgot your password |
|
1207 | msgid "Forgot your password?" | |
1297 | msgstr "Ξεχάσατε τον κωδικό σας;" |
|
1208 | msgstr "Ξεχάσατε τον κωδικό σας;" | |
1298 |
|
1209 | |||
1299 |
msgid "Don't have an account |
|
1210 | msgid "Don't have an account?" | |
1300 | msgstr "Δεν έχετε λογαριασμό;" |
|
1211 | msgstr "Δεν έχετε λογαριασμό;" | |
1301 |
|
1212 | |||
1302 | msgid "Sign In" |
|
1213 | msgid "Sign In" | |
@@ -1788,26 +1699,6 b' msgstr ""' | |||||
1788 | "Ενεργοποιήστε αυτήν την επιλογή ώστε να επιτρέπεται σε μη διαχειριστές να " |
|
1699 | "Ενεργοποιήστε αυτήν την επιλογή ώστε να επιτρέπεται σε μη διαχειριστές να " | |
1789 | "δημιουργούν αποθετήρια στο ανώτερο επίπεδο." |
|
1700 | "δημιουργούν αποθετήρια στο ανώτερο επίπεδο." | |
1790 |
|
1701 | |||
1791 | msgid "" |
|
|||
1792 | "Note: This will also give all users API access to create repositories " |
|
|||
1793 | "everywhere. That might change in future versions." |
|
|||
1794 | msgstr "" |
|
|||
1795 | "Σημείωση: Αυτό θα δώσει επίσης σε όλους τους χρήστες πρόσβαση API για τη " |
|
|||
1796 | "δημιουργία αποθετηρίων παντού. Αυτό μπορεί να αλλάξει σε μελλοντικές " |
|
|||
1797 | "εκδόσεις." |
|
|||
1798 |
|
||||
1799 | msgid "Repository creation with group write access" |
|
|||
1800 | msgstr "Δημιουργία αποθετηρίου με πρόσβαση εγγραφής ομάδας" |
|
|||
1801 |
|
||||
1802 | msgid "" |
|
|||
1803 | "With this, write permission to a repository group allows creating " |
|
|||
1804 | "repositories inside that group. Without this, group write permissions " |
|
|||
1805 | "mean nothing." |
|
|||
1806 | msgstr "" |
|
|||
1807 | "Με αυτό, η άδεια εγγραφής σε μια ομάδα αποθετηρίων επιτρέπει τη " |
|
|||
1808 | "δημιουργία αποθετηρίων εντός αυτής της ομάδας. Χωρίς αυτό, τα δικαιώματα " |
|
|||
1809 | "ομαδικής εγγραφής δεν σημαίνουν τίποτα." |
|
|||
1810 |
|
||||
1811 | msgid "User group creation" |
|
1702 | msgid "User group creation" | |
1812 | msgstr "Δημιουργία ομάδας χρηστών" |
|
1703 | msgstr "Δημιουργία ομάδας χρηστών" | |
1813 |
|
1704 | |||
@@ -2245,12 +2136,6 b' msgstr ""' | |||||
2245 | msgid "Save Settings" |
|
2136 | msgid "Save Settings" | |
2246 | msgstr "Αποθήκευση Ρυθμίσεων" |
|
2137 | msgstr "Αποθήκευση Ρυθμίσεων" | |
2247 |
|
2138 | |||
2248 | msgid "Built-in Mercurial Hooks (Read-Only)" |
|
|||
2249 | msgstr "Ενσωματωμένοι Mercurial Hooks (μόνο για ανάγνωση)" |
|
|||
2250 |
|
||||
2251 | msgid "Custom Hooks" |
|
|||
2252 | msgstr "Προσαρμοσμένα άγκιστρα" |
|
|||
2253 |
|
||||
2254 | msgid "Failed to remove hook" |
|
2139 | msgid "Failed to remove hook" | |
2255 | msgstr "Απέτυχε η αφαίρεση γάντζου" |
|
2140 | msgstr "Απέτυχε η αφαίρεση γάντζου" | |
2256 |
|
2141 | |||
@@ -2279,26 +2164,6 b' msgstr ""' | |||||
2279 | msgid "Install Git hooks" |
|
2164 | msgid "Install Git hooks" | |
2280 | msgstr "Εγκατάσταση Git hooks" |
|
2165 | msgstr "Εγκατάσταση Git hooks" | |
2281 |
|
2166 | |||
2282 | msgid "" |
|
|||
2283 | "Verify if Kallithea's Git hooks are installed for each repository. " |
|
|||
2284 | "Current hooks will be updated to the latest version." |
|
|||
2285 | msgstr "" |
|
|||
2286 | "Επαληθεύστε εάν τα Git hooks της Καλλιθέας είναι εγκατεστημένα για κάθε " |
|
|||
2287 | "αποθετήριο. Τα τρέχοντα hooks θα ενημερωθούν στην τελευταία έκδοση." |
|
|||
2288 |
|
||||
2289 | msgid "Overwrite existing Git hooks" |
|
|||
2290 | msgstr "Αντικατάσταση υπαρχόντων Git hooks" |
|
|||
2291 |
|
||||
2292 | msgid "" |
|
|||
2293 | "If installing Git hooks, overwrite any existing hooks, even if they do " |
|
|||
2294 | "not seem to come from Kallithea. WARNING: This operation will destroy any " |
|
|||
2295 | "custom git hooks you may have deployed by hand!" |
|
|||
2296 | msgstr "" |
|
|||
2297 | "Εάν εγκαθιστάτε Git hooks, αντικαταστήστε τυχόν υπάρχοντα hooks, ακόμα κι " |
|
|||
2298 | "αν δεν φαίνεται να προέρχονται από την Καλλιθέα. ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτή η " |
|
|||
2299 | "λειτουργία θα καταστρέψει τυχόν προσαρμοσμένα git hooks που μπορεί να " |
|
|||
2300 | "έχετε αναπτύξει με το χέρι!" |
|
|||
2301 |
|
||||
2302 | msgid "Rescan Repositories" |
|
2167 | msgid "Rescan Repositories" | |
2303 | msgstr "Επανασάρωση αποθετηρίων" |
|
2168 | msgstr "Επανασάρωση αποθετηρίων" | |
2304 |
|
2169 | |||
@@ -2354,17 +2219,6 b' msgstr "\xce\x95\xcf\x80\xce\xb5\xce\xba\xcf\x84\xce\xac\xcf\x83\xce\xb5\xce\xb9\xcf\x82 Mercurial"' | |||||
2354 | msgid "Enable largefiles extension" |
|
2219 | msgid "Enable largefiles extension" | |
2355 | msgstr "Ενεργοποίηση επέκτασης μεγάλων αρχείων" |
|
2220 | msgstr "Ενεργοποίηση επέκτασης μεγάλων αρχείων" | |
2356 |
|
2221 | |||
2357 | msgid "Enable hgsubversion extension" |
|
|||
2358 | msgstr "Ενεργοποίηση επέκτασης hgsubversion" |
|
|||
2359 |
|
||||
2360 | msgid "" |
|
|||
2361 | "Requires hgsubversion library to be installed. Enables cloning of remote " |
|
|||
2362 | "Subversion repositories while converting them to Mercurial." |
|
|||
2363 | msgstr "" |
|
|||
2364 | "Απαιτεί την εγκατάσταση της βιβλιοθήκης hgsubversion. Ενεργοποιεί την " |
|
|||
2365 | "κλωνοποίηση απομακρυσμένων Subversion αποθετηρίων και τη μετατροπή τους " |
|
|||
2366 | "σε Mercurial." |
|
|||
2367 |
|
||||
2368 | msgid "Location of repositories" |
|
2222 | msgid "Location of repositories" | |
2369 | msgstr "Τοποθεσία αποθετηρίων" |
|
2223 | msgstr "Τοποθεσία αποθετηρίων" | |
2370 |
|
2224 | |||
@@ -2727,9 +2581,6 b' msgstr "\xce\xa3\xcf\x85\xce\xbd\xce\xb4\xce\xb5\xce\xb8\xce\xb5\xce\xaf\xcf\x84\xce\xb5 \xcf\x83\xcf\x84\xce\xbf \xce\xbb\xce\xbf\xce\xb3\xce\xb1\xcf\x81\xce\xb9\xce\xb1\xcf\x83\xce\xbc\xcf\x8c \xcf\x83\xce\xb1\xcf\x82"' | |||||
2727 | msgid "Forgot password?" |
|
2581 | msgid "Forgot password?" | |
2728 | msgstr "Ξεχάσατε τον κωδικό πρόσβασης;" |
|
2582 | msgstr "Ξεχάσατε τον κωδικό πρόσβασης;" | |
2729 |
|
2583 | |||
2730 | msgid "Don't have an account?" |
|
|||
2731 | msgstr "Δεν έχετε λογαριασμό;" |
|
|||
2732 |
|
||||
2733 | msgid "Log Out" |
|
2584 | msgid "Log Out" | |
2734 | msgstr "Αποσύνδεση" |
|
2585 | msgstr "Αποσύνδεση" | |
2735 |
|
2586 | |||
@@ -2840,7 +2691,7 b' msgstr ""' | |||||
2840 | msgid "Failed to revoke permission" |
|
2691 | msgid "Failed to revoke permission" | |
2841 | msgstr "Απέτυχε η ανάκληση του δικαιωμάτος" |
|
2692 | msgstr "Απέτυχε η ανάκληση του δικαιωμάτος" | |
2842 |
|
2693 | |||
2843 |
msgid "Confirm to revoke permission for {0}: {1} |
|
2694 | msgid "Confirm to revoke permission for {0}: {1}?" | |
2844 | msgstr "Επιβεβαιώστε την ανάκληση του δικαιώματος για {0}: {1};" |
|
2695 | msgstr "Επιβεβαιώστε την ανάκληση του δικαιώματος για {0}: {1};" | |
2845 |
|
2696 | |||
2846 | msgid "Select changeset" |
|
2697 | msgid "Select changeset" | |
@@ -3260,6 +3111,9 b' msgstr "%s \xce\x91\xcf\x81\xcf\x87\xce\xb5\xce\xaf\xce\xbf \xce\xb4\xce\xb9\xce\xb1\xcf\x86\xce\xbf\xcf\x81\xce\xac\xcf\x82 \xce\xb4\xce\xaf\xcf\x80\xce\xbb\xce\xb1-\xce\xb4\xce\xaf\xcf\x80\xce\xbb\xce\xb1"' | |||||
3260 | msgid "File diff" |
|
3111 | msgid "File diff" | |
3261 | msgstr "Αρχείο διαφοράς" |
|
3112 | msgstr "Αρχείο διαφοράς" | |
3262 |
|
3113 | |||
|
3114 | msgid "Ignore whitespace" | |||
|
3115 | msgstr "Αγνόηση κενού" | |||
|
3116 | ||||
3263 | msgid "%s File Diff" |
|
3117 | msgid "%s File Diff" | |
3264 | msgstr "%s Αρχείο διαφοράς" |
|
3118 | msgstr "%s Αρχείο διαφοράς" | |
3265 |
|
3119 |
@@ -19,15 +19,6 b' msgstr "Ninguno"' | |||||
19 | msgid "(closed)" |
|
19 | msgid "(closed)" | |
20 | msgstr "(cerrado)" |
|
20 | msgstr "(cerrado)" | |
21 |
|
21 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Mostrar espacios en blanco" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Ignorar espacios en blanco" |
|
|||
27 |
|
||||
28 | msgid "Increase diff context to %(num)s lines" |
|
|||
29 | msgstr "Aumentar el contexto del diff a %(num)s lineas" |
|
|||
30 |
|
||||
31 | msgid "Successfully deleted pull request %s" |
|
22 | msgid "Successfully deleted pull request %s" | |
32 | msgstr "Petición de pull %s eliminada correctamente" |
|
23 | msgstr "Petición de pull %s eliminada correctamente" | |
33 |
|
24 | |||
@@ -280,5 +271,11 b' msgstr "Sin modificar"' | |||||
280 | msgid "Successfully updated gist content" |
|
271 | msgid "Successfully updated gist content" | |
281 | msgstr "Gist actualizado correctamente" |
|
272 | msgstr "Gist actualizado correctamente" | |
282 |
|
273 | |||
|
274 | msgid "Increase diff context to %(num)s lines" | |||
|
275 | msgstr "Aumentar el contexto del diff a %(num)s lineas" | |||
|
276 | ||||
283 | msgid "Select changeset" |
|
277 | msgid "Select changeset" | |
284 | msgstr "Seleccionar cambios" |
|
278 | msgstr "Seleccionar cambios" | |
|
279 | ||||
|
280 | msgid "Ignore whitespace" | |||
|
281 | msgstr "Ignorar espacios en blanco" |
@@ -10,24 +10,30 b' msgstr ""' | |||||
10 | "Content-Transfer-Encoding: 8bit\n" |
|
10 | "Content-Transfer-Encoding: 8bit\n" | |
11 | "Plural-Forms: nplurals=2; plural=n > 1;\n" |
|
11 | "Plural-Forms: nplurals=2; plural=n > 1;\n" | |
12 |
|
12 | |||
|
13 | msgid "" | |||
|
14 | "CSRF token leak has been detected - all form tokens have been expired" | |||
|
15 | msgstr "" | |||
|
16 | "Une fuite de jeton CSRF a été détectée - tous les jetons de formulaire " | |||
|
17 | "sont considérés comme expirés" | |||
|
18 | ||||
|
19 | msgid "Repository not found in the filesystem" | |||
|
20 | msgstr "Dépôt non trouvé sur le système de fichiers" | |||
|
21 | ||||
13 | msgid "There are no changesets yet" |
|
22 | msgid "There are no changesets yet" | |
14 | msgstr "Il n’y a aucun changement pour le moment" |
|
23 | msgstr "Il n’y a aucun changement pour le moment" | |
15 |
|
24 | |||
|
25 | msgid "Changeset for %s %s not found in %s" | |||
|
26 | msgstr "Ensemble de changements pour %s %s non trouvé dans %s" | |||
|
27 | ||||
|
28 | msgid "SSH access is disabled." | |||
|
29 | msgstr "L'accès SSH est désactivé." | |||
|
30 | ||||
16 | msgid "None" |
|
31 | msgid "None" | |
17 | msgstr "Aucun" |
|
32 | msgstr "Aucun" | |
18 |
|
33 | |||
19 | msgid "(closed)" |
|
34 | msgid "(closed)" | |
20 | msgstr "(fermé)" |
|
35 | msgstr "(fermé)" | |
21 |
|
36 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Afficher les espaces et tabulations" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Ignorer les espaces et tabulations" |
|
|||
27 |
|
||||
28 | msgid "Increase diff context to %(num)s lines" |
|
|||
29 | msgstr "Augmenter le contexte du diff à %(num)s lignes" |
|
|||
30 |
|
||||
31 | msgid "No permission to change status" |
|
37 | msgid "No permission to change status" | |
32 | msgstr "Permission manquante pour changer le statut" |
|
38 | msgstr "Permission manquante pour changer le statut" | |
33 |
|
39 | |||
@@ -549,13 +555,6 b' msgstr ""' | |||||
549 | msgid "Updated VCS settings" |
|
555 | msgid "Updated VCS settings" | |
550 | msgstr "Réglages des gestionnaires de versions mis à jour" |
|
556 | msgstr "Réglages des gestionnaires de versions mis à jour" | |
551 |
|
557 | |||
552 | msgid "" |
|
|||
553 | "Unable to activate hgsubversion support. The \"hgsubversion\" library is " |
|
|||
554 | "missing" |
|
|||
555 | msgstr "" |
|
|||
556 | "Impossible d'activer la prise en charge de hgsubversion. La bibliothèque " |
|
|||
557 | "« hgsubversion » est manquante" |
|
|||
558 |
|
||||
559 | msgid "Error occurred while updating application settings" |
|
558 | msgid "Error occurred while updating application settings" | |
560 | msgstr "" |
|
559 | msgstr "" | |
561 | "Une erreur est survenue durant la mise à jour des réglages de " |
|
560 | "Une erreur est survenue durant la mise à jour des réglages de " | |
@@ -587,10 +586,12 b' msgstr "T\xc3\xa2che d\'envoi d\'e-mail cr\xc3\xa9\xc3\xa9e"' | |||||
587 | msgid "Hook already exists" |
|
586 | msgid "Hook already exists" | |
588 | msgstr "Le hook existe déjà" |
|
587 | msgstr "Le hook existe déjà" | |
589 |
|
588 | |||
590 | msgid "Builtin hooks are read-only. Please use another hook name." |
|
589 | msgid "" | |
|
590 | "Hook names with \".kallithea_\" are reserved for internal use. Please use " | |||
|
591 | "another hook name." | |||
591 | msgstr "" |
|
592 | msgstr "" | |
592 | "Les hooks intégrés sont en lecture seule. Merci de choisir un autre nom " |
|
593 | "Les noms de hook avec \".kallithea_\" sont réservés pour un usage interne. " | |
593 | "pour le hook." |
|
594 | "Merci de choisir un autre nom pour le hook." | |
594 |
|
595 | |||
595 | msgid "Added new hook" |
|
596 | msgid "Added new hook" | |
596 | msgstr "Le nouveau hook a été ajouté" |
|
597 | msgstr "Le nouveau hook a été ajouté" | |
@@ -671,21 +672,6 b' msgstr ""' | |||||
671 | msgid "You need to be signed in to view this page" |
|
672 | msgid "You need to be signed in to view this page" | |
672 | msgstr "Vous devez être connecté pour visualiser cette page" |
|
673 | msgstr "Vous devez être connecté pour visualiser cette page" | |
673 |
|
674 | |||
674 | msgid "" |
|
|||
675 | "CSRF token leak has been detected - all form tokens have been expired" |
|
|||
676 | msgstr "" |
|
|||
677 | "Une fuite de jeton CSRF a été détectée - tous les jetons de formulaire " |
|
|||
678 | "sont considérés comme expirés" |
|
|||
679 |
|
||||
680 | msgid "Repository not found in the filesystem" |
|
|||
681 | msgstr "Dépôt non trouvé sur le système de fichiers" |
|
|||
682 |
|
||||
683 | msgid "Changeset for %s %s not found in %s" |
|
|||
684 | msgstr "Ensemble de changements pour %s %s non trouvé dans %s" |
|
|||
685 |
|
||||
686 | msgid "SSH access is disabled." |
|
|||
687 | msgstr "L'accès SSH est désactivé." |
|
|||
688 |
|
||||
689 | msgid "Binary file" |
|
675 | msgid "Binary file" | |
690 | msgstr "Fichier binaire" |
|
676 | msgstr "Fichier binaire" | |
691 |
|
677 | |||
@@ -698,6 +684,15 b' msgstr ""' | |||||
698 | msgid "No changes detected" |
|
684 | msgid "No changes detected" | |
699 | msgstr "Aucun changement détecté" |
|
685 | msgstr "Aucun changement détecté" | |
700 |
|
686 | |||
|
687 | msgid "Show whitespace changes" | |||
|
688 | msgstr "Afficher les modifications d'espaces et de tabulations" | |||
|
689 | ||||
|
690 | msgid "Ignore whitespace changes" | |||
|
691 | msgstr "Ignorer les modifications d'espaces et de tabulations" | |||
|
692 | ||||
|
693 | msgid "Increase diff context to %(num)s lines" | |||
|
694 | msgstr "Augmenter le contexte du diff à %(num)s lignes" | |||
|
695 | ||||
701 | msgid "Deleted branch: %s" |
|
696 | msgid "Deleted branch: %s" | |
702 | msgstr "Branche supprimée : %s" |
|
697 | msgstr "Branche supprimée : %s" | |
703 |
|
698 | |||
@@ -809,40 +804,55 b' msgstr "renommer"' | |||||
809 | msgid "chmod" |
|
804 | msgid "chmod" | |
810 | msgstr "chmod" |
|
805 | msgstr "chmod" | |
811 |
|
806 | |||
812 | msgid "" |
|
|||
813 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
814 | "the filesystem please run the application again in order to rescan " |
|
|||
815 | "repositories" |
|
|||
816 | msgstr "" |
|
|||
817 | "Le dépôt %s n’est pas représenté dans la base de données. Il a " |
|
|||
818 | "probablement été créé ou renommé manuellement. Veuillez relancer " |
|
|||
819 | "l’application pour rescanner les dépôts" |
|
|||
820 |
|
||||
821 | msgid "SSH key is missing" |
|
807 | msgid "SSH key is missing" | |
822 | msgstr "La clé SSH est manquante" |
|
808 | msgstr "La clé SSH est manquante" | |
823 |
|
809 | |||
824 | msgid "" |
|
810 | msgid "" | |
825 |
"In |
|
811 | "Invalid SSH key - it must have both a key type and a base64 part, like " | |
826 | "'ssh-rsa ASRNeaZu4FA...xlJp='" |
|
812 | "'ssh-rsa ASRNeaZu4FA...xlJp='" | |
827 | msgstr "" |
|
813 | msgstr "" | |
828 |
"Clé SSH in |
|
814 | "Clé SSH invalide – elle doit comporter à la fois un type de clé et une " | |
829 | "partie base64, comme 'ssh-rsa ASRNeaZu4FA...xlJp='" |
|
815 | "partie base64, comme 'ssh-rsa ASRNeaZu4FA...xlJp='" | |
830 |
|
816 | |||
831 | msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'" |
|
817 | msgid "" | |
|
818 | "Invalid SSH key - it must start with key type 'ssh-rsa', 'ssh-dss', 'ssh-" | |||
|
819 | "ed448', or 'ssh-ed25519'" | |||
832 | msgstr "" |
|
820 | msgstr "" | |
833 |
"Clé SSH in |
|
821 | "Clé SSH invalide – elle doit commencer par le type de clé 'ssh-rsa', 'ssh-" | |
834 |
|
822 | "dss', 'ssh-ed448', ou 'ssh-ed25519'" | ||
835 | msgid "Incorrect SSH key - unexpected characters in base64 part %r" |
|
823 | ||
|
824 | msgid "Invalid SSH key - unexpected characters in base64 part %r" | |||
|
825 | msgstr "Clé SSH invalide – caractères inattendus dans la partie base 64 %r" | |||
|
826 | ||||
|
827 | msgid "" | |||
|
828 | "Invalid SSH key - base64 part %r seems truncated (it can't be decoded)" | |||
836 | msgstr "" |
|
829 | msgstr "" | |
837 | "Clé SSH incorrecte – caractères inattendus dans la partie base 64 %r" |
|
830 | "Clé SSH invalide – la partie base64 %r semble tronquée (elle ne peut pas " | |
838 |
|
831 | "être décodée)" | ||
839 | msgid "Incorrect SSH key - failed to decode base64 part %r" |
|
832 | ||
840 | msgstr "Clé SSH incorrecte – échec du décodage de la partie base64 %r" |
|
833 | msgid "" | |
841 |
|
834 | "Invalid SSH key - base64 part %r seems truncated (it contains a partial " | ||
842 | msgid "Incorrect SSH key - base64 part is not %r as claimed but %r" |
|
835 | "string length)" | |
843 | msgstr "" |
|
836 | msgstr "" | |
844 | "Clé SSH incorrecte – la partie base 64 n'est pas %r comme il est dit mais " |
|
837 | "Clé SSH invalide – la partie base64 %r semble tronquée (elle contient une " | |
845 | "%r" |
|
838 | "taille partielle)" | |
|
839 | ||||
|
840 | msgid "" | |||
|
841 | "Invalid SSH key - base64 part %r seems truncated (it is too short for " | |||
|
842 | "declared string length %s)" | |||
|
843 | msgstr "" | |||
|
844 | "Clé SSH invalide – la partie base64 %r semble tronquée (elle est trop court " | |||
|
845 | "pour la taille déclarée %s)" | |||
|
846 | ||||
|
847 | msgid "" | |||
|
848 | "Invalid SSH key - base64 part %r seems truncated (it contains too few " | |||
|
849 | "strings for a %s key)" | |||
|
850 | msgstr "" | |||
|
851 | "Clé SSH invalide – la partie base64 %r semble tronquée (elle ne contient pas " | |||
|
852 | "assez de parties pour une clé %s)" | |||
|
853 | ||||
|
854 | msgid "Invalid SSH key - it is a %s key but the base64 part contains %r" | |||
|
855 | msgstr "Clé SSH invalide – c'est une clé %s mais la partie base64 contient %r" | |||
846 |
|
856 | |||
847 | msgid "%d year" |
|
857 | msgid "%d year" | |
848 | msgid_plural "%d years" |
|
858 | msgid_plural "%d years" | |
@@ -889,12 +899,6 b' msgstr "Il y a %s et %s"' | |||||
889 | msgid "just now" |
|
899 | msgid "just now" | |
890 | msgstr "à l’instant" |
|
900 | msgstr "à l’instant" | |
891 |
|
901 | |||
892 | msgid "on line %s" |
|
|||
893 | msgstr "à la ligne %s" |
|
|||
894 |
|
||||
895 | msgid "[Mention]" |
|
|||
896 | msgstr "[Mention]" |
|
|||
897 |
|
||||
898 | msgid "top level" |
|
902 | msgid "top level" | |
899 | msgstr "niveau supérieur" |
|
903 | msgstr "niveau supérieur" | |
900 |
|
904 | |||
@@ -952,13 +956,6 b' msgstr ""' | |||||
952 | "L'utilisateur par défaut a un accès administrateur aux nouveaux groupes " |
|
956 | "L'utilisateur par défaut a un accès administrateur aux nouveaux groupes " | |
953 | "d'utilisateurs" |
|
957 | "d'utilisateurs" | |
954 |
|
958 | |||
955 | msgid "Only admins can create repository groups" |
|
|||
956 | msgstr "Seul un administrateur peut créer un groupe de dépôts" |
|
|||
957 |
|
||||
958 | msgid "Non-admins can create repository groups" |
|
|||
959 | msgstr "" |
|
|||
960 | "Les utilisateurs non-administrateurs peuvent créer des groupes de dépôts" |
|
|||
961 |
|
||||
962 | msgid "Only admins can create user groups" |
|
959 | msgid "Only admins can create user groups" | |
963 | msgstr "Seul un administrateur peut créer des groupes d'utilisateurs" |
|
960 | msgstr "Seul un administrateur peut créer des groupes d'utilisateurs" | |
964 |
|
961 | |||
@@ -975,18 +972,6 b' msgstr ""' | |||||
975 | "Les utilisateurs non-administrateurs peuvent créer des dépôts de niveau " |
|
972 | "Les utilisateurs non-administrateurs peuvent créer des dépôts de niveau " | |
976 | "supérieur" |
|
973 | "supérieur" | |
977 |
|
974 | |||
978 | msgid "" |
|
|||
979 | "Repository creation enabled with write permission to a repository group" |
|
|||
980 | msgstr "" |
|
|||
981 | "Création de dépôts activée avec l'accès en écriture vers un groupe de " |
|
|||
982 | "dépôts" |
|
|||
983 |
|
||||
984 | msgid "" |
|
|||
985 | "Repository creation disabled with write permission to a repository group" |
|
|||
986 | msgstr "" |
|
|||
987 | "Création de dépôts désactivée avec l'accès en écriture vers un groupe de " |
|
|||
988 | "dépôts" |
|
|||
989 |
|
||||
990 | msgid "Only admins can fork repositories" |
|
975 | msgid "Only admins can fork repositories" | |
991 | msgstr "Seul un administrateur peut faire un fork de dépôt" |
|
976 | msgstr "Seul un administrateur peut faire un fork de dépôt" | |
992 |
|
977 | |||
@@ -1032,10 +1017,10 b' msgstr "Le nom ne doit pas contenir seul' | |||||
1032 |
|
1017 | |||
1033 | msgid "" |
|
1018 | msgid "" | |
1034 | "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on " |
|
1019 | "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on " | |
1035 | "%(branch)s" |
|
1020 | "%(branch)s by %(cs_author_username)s" | |
1036 | msgstr "" |
|
1021 | msgstr "" | |
1037 | "[Commentaire] Changeset %(short_id)s « %(message_short)s » de " |
|
1022 | "[Commentaire] Changeset %(short_id)s « %(message_short)s » de %(repo_name)s " | |
1038 | "%(repo_name)s dans %(branch)s" |
|
1023 | "dans %(branch)s par %(cs_author_username)s" | |
1039 |
|
1024 | |||
1040 | msgid "New user %(new_username)s registered" |
|
1025 | msgid "New user %(new_username)s registered" | |
1041 | msgstr "Nouvel utilisateur %(new_username)s enregistré" |
|
1026 | msgstr "Nouvel utilisateur %(new_username)s enregistré" | |
@@ -1057,12 +1042,6 b' msgstr ""' | |||||
1057 | msgid "Closing" |
|
1042 | msgid "Closing" | |
1058 | msgstr "Fermeture" |
|
1043 | msgstr "Fermeture" | |
1059 |
|
1044 | |||
1060 | msgid "" |
|
|||
1061 | "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s" |
|
|||
1062 | msgstr "" |
|
|||
1063 | "%(user)s veut que vous regardiez la demande de pull %(pr_nice_id)s : " |
|
|||
1064 | "%(pr_title)s" |
|
|||
1065 |
|
||||
1066 | msgid "Cannot create empty pull request" |
|
1045 | msgid "Cannot create empty pull request" | |
1067 | msgstr "Impossible de créer une requête de pull vide" |
|
1046 | msgstr "Impossible de créer une requête de pull vide" | |
1068 |
|
1047 | |||
@@ -1110,9 +1089,6 b' msgstr "La cl\xc3\xa9 SSH %s est d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e par %s"' | |||||
1110 | msgid "SSH key with fingerprint %r found" |
|
1089 | msgid "SSH key with fingerprint %r found" | |
1111 | msgstr "Clé SSH avec l'empreinte %r trouvée" |
|
1090 | msgstr "Clé SSH avec l'empreinte %r trouvée" | |
1112 |
|
1091 | |||
1113 | msgid "New user registration" |
|
|||
1114 | msgstr "Nouveau enregistrement d'utilisateur" |
|
|||
1115 |
|
||||
1116 | msgid "" |
|
1092 | msgid "" | |
1117 | "You can't remove this user since it is crucial for the entire application" |
|
1093 | "You can't remove this user since it is crucial for the entire application" | |
1118 | msgstr "" |
|
1094 | msgstr "" | |
@@ -1227,12 +1203,9 b' msgstr "Un groupe de d\xc3\xa9p\xc3\xb4ts avec le nom \xc2\xab\xe2\x80\xaf%(repo)s\xe2\x80\xaf\xc2\xbb existe d\xc3\xa9j\xc3\xa0"' | |||||
1227 | msgid "Invalid repository URL" |
|
1203 | msgid "Invalid repository URL" | |
1228 | msgstr "URL de dépôt invalide" |
|
1204 | msgstr "URL de dépôt invalide" | |
1229 |
|
1205 | |||
1230 | msgid "" |
|
1206 | msgid "Invalid repository URL. It must be a valid http, https, or ssh URL" | |
1231 | "Invalid repository URL. It must be a valid http, https, ssh, svn+http or " |
|
|||
1232 | "svn+https URL" |
|
|||
1233 | msgstr "" |
|
1207 | msgstr "" | |
1234 |
"URL de dépôt invalide. Ce doit être une URL valide de type http, https |
|
1208 | "URL de dépôt invalide. Ce doit être une URL valide de type http, https ou ssh" | |
1235 | "ssh, svn+http ou svn+https" |
|
|||
1236 |
|
1209 | |||
1237 | msgid "Fork has to be the same type as parent" |
|
1210 | msgid "Fork has to be the same type as parent" | |
1238 | msgstr "Le fork doit être du même type que le parent" |
|
1211 | msgstr "Le fork doit être du même type que le parent" | |
@@ -1334,10 +1307,10 b' msgstr "Mot de passe"' | |||||
1334 | msgid "Stay logged in after browser restart" |
|
1307 | msgid "Stay logged in after browser restart" | |
1335 | msgstr "Rester connecté après un redémarrage du navigateur" |
|
1308 | msgstr "Rester connecté après un redémarrage du navigateur" | |
1336 |
|
1309 | |||
1337 |
msgid "Forgot your password |
|
1310 | msgid "Forgot your password?" | |
1338 |
msgstr "Mot de passe oublié |
|
1311 | msgstr "Mot de passe oublié?" | |
1339 |
|
1312 | |||
1340 |
msgid "Don't have an account |
|
1313 | msgid "Don't have an account?" | |
1341 | msgstr "Vous n’avez pas de compte ?" |
|
1314 | msgstr "Vous n’avez pas de compte ?" | |
1342 |
|
1315 | |||
1343 | msgid "Sign In" |
|
1316 | msgid "Sign In" | |
@@ -1826,26 +1799,6 b' msgstr ""' | |||||
1826 | "Activer pour autoriser les non-administrateurs à créer des dépôts au " |
|
1799 | "Activer pour autoriser les non-administrateurs à créer des dépôts au " | |
1827 | "niveau supérieur." |
|
1800 | "niveau supérieur." | |
1828 |
|
1801 | |||
1829 | msgid "" |
|
|||
1830 | "Note: This will also give all users API access to create repositories " |
|
|||
1831 | "everywhere. That might change in future versions." |
|
|||
1832 | msgstr "" |
|
|||
1833 | "Note : Cela autorisera également tous les utilisateurs à utiliser l'API " |
|
|||
1834 | "pour créer des dépôts partout. Ce comportement peut changer dans des " |
|
|||
1835 | "versions futures." |
|
|||
1836 |
|
||||
1837 | msgid "Repository creation with group write access" |
|
|||
1838 | msgstr "Création de dépôts avec l'accès en écriture du groupe" |
|
|||
1839 |
|
||||
1840 | msgid "" |
|
|||
1841 | "With this, write permission to a repository group allows creating " |
|
|||
1842 | "repositories inside that group. Without this, group write permissions " |
|
|||
1843 | "mean nothing." |
|
|||
1844 | msgstr "" |
|
|||
1845 | "Avec ceci, le droit d'écriture dans un groupe de dépôt donne le droit de " |
|
|||
1846 | "créer des dépôts dans ce groupe. Sans ceci, le droit d'écriture pour les " |
|
|||
1847 | "groupes n'a pas d'impact." |
|
|||
1848 |
|
||||
1849 | msgid "User group creation" |
|
1802 | msgid "User group creation" | |
1850 | msgstr "Création de groupes d'utilisateurs" |
|
1803 | msgstr "Création de groupes d'utilisateurs" | |
1851 |
|
1804 | |||
@@ -2276,11 +2229,8 b' msgstr ""' | |||||
2276 | msgid "Save Settings" |
|
2229 | msgid "Save Settings" | |
2277 | msgstr "Enregistrer les options" |
|
2230 | msgstr "Enregistrer les options" | |
2278 |
|
2231 | |||
2279 |
msgid " |
|
2232 | msgid "Custom Global Mercurial Hooks" | |
2280 | msgstr "Hooks Mercurial intégrés (lecture seule)" |
|
2233 | msgstr "Hooks Mercurial globaux personnalisés" | |
2281 |
|
||||
2282 | msgid "Custom Hooks" |
|
|||
2283 | msgstr "Hooks personnalisés" |
|
|||
2284 |
|
2234 | |||
2285 | msgid "" |
|
2235 | msgid "" | |
2286 | "Hooks can be used to trigger actions on certain events such as push / " |
|
2236 | "Hooks can be used to trigger actions on certain events such as push / " | |
@@ -2290,6 +2240,21 b' msgstr ""' | |||||
2290 | "certains évènements comme le push et le pull. Ils peuvent déclencher des " |
|
2240 | "certains évènements comme le push et le pull. Ils peuvent déclencher des " | |
2291 | "fonctions Python ou des applications externes." |
|
2241 | "fonctions Python ou des applications externes." | |
2292 |
|
2242 | |||
|
2243 | msgid "Git Hooks" | |||
|
2244 | msgstr "Git Hooks" | |||
|
2245 | ||||
|
2246 | msgid "" | |||
|
2247 | "Kallithea has no support for custom Git hooks. Kallithea will use Git " | |||
|
2248 | "post-receive hooks internally. Installation of these hooks is managed in " | |||
|
2249 | "%s." | |||
|
2250 | msgstr "" | |||
|
2251 | "Kallithea ne supporte pas les hooks Git personnalisés. Kallithea utilise des " | |||
|
2252 | "hooks Git de post-réception en interne. L'installation de ces hooks est " | |||
|
2253 | "gérée dans %s." | |||
|
2254 | ||||
|
2255 | msgid "Custom Hooks are not enabled" | |||
|
2256 | msgstr "Les Hooks personnalisés ne sont pas activés" | |||
|
2257 | ||||
2293 | msgid "Failed to remove hook" |
|
2258 | msgid "Failed to remove hook" | |
2294 | msgstr "Erreur lors de la suppression du hook" |
|
2259 | msgstr "Erreur lors de la suppression du hook" | |
2295 |
|
2260 | |||
@@ -2318,23 +2283,25 b' msgid "Install Git hooks"' | |||||
2318 | msgstr "Installer des hooks Git" |
|
2283 | msgstr "Installer des hooks Git" | |
2319 |
|
2284 | |||
2320 | msgid "" |
|
2285 | msgid "" | |
2321 | "Verify if Kallithea's Git hooks are installed for each repository. " |
|
2286 | "Install Kallithea's internal hooks for all Git repositories where they " | |
2322 | "Current hooks will be updated to the latest version." |
|
2287 | "are missing or can be upgraded. Existing hooks that don't seem to come " | |
|
2288 | "from Kallithea will not be touched." | |||
2323 | msgstr "" |
|
2289 | msgstr "" | |
2324 | "Vérifier si les hooks Git de Kallithea sont installés pour chaque dépôt. " |
|
2290 | "Installe les hooks internes de Kallithea pour tous les dépôts Git où ils " | |
2325 | "Les hooks actuels seront mis à jour vers la dernière version." |
|
2291 | "sont absents ou s'ils peuvent être mis à jour. Les hooks existants qui ne " | |
2326 |
|
2292 | "semblent pas être livrés avec Kallithea ne seront pas impactés." | ||
2327 | msgid "Overwrite existing Git hooks" |
|
2293 | ||
2328 | msgstr "Écraser les hooks Git existants" |
|
2294 | msgid "Install and overwrite Git hooks" | |
|
2295 | msgstr "Installer et surcharger des hooks Git" | |||
2329 |
|
2296 | |||
2330 | msgid "" |
|
2297 | msgid "" | |
2331 | "If installing Git hooks, overwrite any existing hooks, even if they do " |
|
2298 | "Install Kallithea's internal hooks for all Git repositories. Existing " | |
2332 | "not seem to come from Kallithea. WARNING: This operation will destroy any " |
|
2299 | "hooks that don't seem to come from Kallithea will be disabled by renaming " | |
2333 | "custom git hooks you may have deployed by hand!" |
|
2300 | "to .bak extension." | |
2334 | msgstr "" |
|
2301 | msgstr "" | |
2335 | "Lors de l'installation des hooks Git, écraser tous les hooks existants, " |
|
2302 | "Installe les hooks internes de Kallithea pour tous les dépôts Git. Les hooks " | |
2336 | "même s'ils ne semblent pas provenir de Kallithea. ATTENTION : cette " |
|
2303 | "existants qui ne semblent pas être livrés avec Kallithea seront désactivés " | |
2337 | "opération détruira tous les hooks Git que vous avez déployés à la main !" |
|
2304 | "en les renommant avec l'extension .bak." | |
2338 |
|
2305 | |||
2339 | msgid "Rescan Repositories" |
|
2306 | msgid "Rescan Repositories" | |
2340 | msgstr "Relancer le scan des dépôts" |
|
2307 | msgstr "Relancer le scan des dépôts" | |
@@ -2379,6 +2346,9 b' msgstr "Chemin de Git"' | |||||
2379 | msgid "Python Packages" |
|
2346 | msgid "Python Packages" | |
2380 | msgstr "Paquets Python" |
|
2347 | msgstr "Paquets Python" | |
2381 |
|
2348 | |||
|
2349 | msgid "Mercurial Push Hooks" | |||
|
2350 | msgstr "Hooks Push Mercurial" | |||
|
2351 | ||||
2382 | msgid "Show repository size after push" |
|
2352 | msgid "Show repository size after push" | |
2383 | msgstr "Afficher la taille du dépôt après un push" |
|
2353 | msgstr "Afficher la taille du dépôt après un push" | |
2384 |
|
2354 | |||
@@ -2391,16 +2361,6 b' msgstr "Extensions Mercurial"' | |||||
2391 | msgid "Enable largefiles extension" |
|
2361 | msgid "Enable largefiles extension" | |
2392 | msgstr "Activer l'extension largefiles" |
|
2362 | msgstr "Activer l'extension largefiles" | |
2393 |
|
2363 | |||
2394 | msgid "Enable hgsubversion extension" |
|
|||
2395 | msgstr "Activer l'extension hgsubversion" |
|
|||
2396 |
|
||||
2397 | msgid "" |
|
|||
2398 | "Requires hgsubversion library to be installed. Enables cloning of remote " |
|
|||
2399 | "Subversion repositories while converting them to Mercurial." |
|
|||
2400 | msgstr "" |
|
|||
2401 | "La bibliothèque hgsubversion doit être installée. Elle permet de cloner " |
|
|||
2402 | "des dépôts SVN distants et de les migrer vers Mercurial." |
|
|||
2403 |
|
||||
2404 | msgid "Location of repositories" |
|
2364 | msgid "Location of repositories" | |
2405 | msgstr "Emplacement des dépôts" |
|
2365 | msgstr "Emplacement des dépôts" | |
2406 |
|
2366 | |||
@@ -2472,6 +2432,47 b' msgstr ""' | |||||
2472 | "emplacement réseau/hôte du serveur Kallithea en cours d'utilisation." |
|
2432 | "emplacement réseau/hôte du serveur Kallithea en cours d'utilisation." | |
2473 |
|
2433 | |||
2474 | msgid "" |
|
2434 | msgid "" | |
|
2435 | "Schema of clone URL construction eg. '{scheme}://{user}@{netloc}/" | |||
|
2436 | "{repo}'.\n" | |||
|
2437 | " The following " | |||
|
2438 | "variables are available:\n" | |||
|
2439 | " {scheme} 'http' or " | |||
|
2440 | "'https' sent from running Kallithea server,\n" | |||
|
2441 | " {user} current user " | |||
|
2442 | "username,\n" | |||
|
2443 | " {netloc} network " | |||
|
2444 | "location/server host of running Kallithea server,\n" | |||
|
2445 | " {repo} full " | |||
|
2446 | "repository name,\n" | |||
|
2447 | " {repoid} ID of " | |||
|
2448 | "repository, can be used to construct clone-by-id,\n" | |||
|
2449 | " {system_user} name " | |||
|
2450 | "of the Kallithea system user,\n" | |||
|
2451 | " {hostname} server " | |||
|
2452 | "hostname\n" | |||
|
2453 | " " | |||
|
2454 | msgstr "" | |||
|
2455 | "Modèle de construction d'URL de clone. Par exemple : " | |||
|
2456 | "'{scheme}://{user}@{netloc}/{repo}'.\n" | |||
|
2457 | " Les variables " | |||
|
2458 | "suivantes sont disponibles :\n" | |||
|
2459 | " {scheme} 'http' " | |||
|
2460 | "ou 'https' envoyé à partir du serveur Kallithea en cours d'utilisation,\n" | |||
|
2461 | " {user} nom de " | |||
|
2462 | "l'utilisateur courant,\n" | |||
|
2463 | " {netloc} " | |||
|
2464 | "emplacement réseau/hôte du serveur Kallithea en cours d'utilisation,\n" | |||
|
2465 | " {repo} nom " | |||
|
2466 | "complet du dépôt,\n" | |||
|
2467 | " {repoid} ID du " | |||
|
2468 | "dépôt, peut être utilisé pour cloner par ID,\n" | |||
|
2469 | " {system_user} nom " | |||
|
2470 | "de l'utilisateur système Kallithea,\n" | |||
|
2471 | " {hostname} nom " | |||
|
2472 | "d'hôte du serveur\n" | |||
|
2473 | " " | |||
|
2474 | ||||
|
2475 | msgid "" | |||
2475 | "Schema for constructing SSH clone URL, eg. 'ssh://{system_user}" |
|
2476 | "Schema for constructing SSH clone URL, eg. 'ssh://{system_user}" | |
2476 | "@{hostname}/{repo}'." |
|
2477 | "@{hostname}/{repo}'." | |
2477 | msgstr "" |
|
2478 | msgstr "" | |
@@ -2715,9 +2716,6 b' msgstr "Connexion \xc3\xa0 votre compte"' | |||||
2715 | msgid "Forgot password?" |
|
2716 | msgid "Forgot password?" | |
2716 | msgstr "Mot de passe oublié ?" |
|
2717 | msgstr "Mot de passe oublié ?" | |
2717 |
|
2718 | |||
2718 | msgid "Don't have an account?" |
|
|||
2719 | msgstr "Vous n’avez pas de compte ?" |
|
|||
2720 |
|
||||
2721 | msgid "Log Out" |
|
2719 | msgid "Log Out" | |
2722 | msgstr "Se déconnecter" |
|
2720 | msgstr "Se déconnecter" | |
2723 |
|
2721 | |||
@@ -2827,7 +2825,7 b' msgstr ""' | |||||
2827 | msgid "Failed to revoke permission" |
|
2825 | msgid "Failed to revoke permission" | |
2828 | msgstr "Échec de la révocation de permission" |
|
2826 | msgstr "Échec de la révocation de permission" | |
2829 |
|
2827 | |||
2830 |
msgid "Confirm to revoke permission for {0}: {1} |
|
2828 | msgid "Confirm to revoke permission for {0}: {1}?" | |
2831 | msgstr "Voulez-vous vraiment révoquer la permission pour {0} : {1} ?" |
|
2829 | msgstr "Voulez-vous vraiment révoquer la permission pour {0} : {1} ?" | |
2832 |
|
2830 | |||
2833 | msgid "Select changeset" |
|
2831 | msgid "Select changeset" | |
@@ -2908,6 +2906,9 b' msgstr ""' | |||||
2908 | msgid "Changeset status: %s by %s" |
|
2906 | msgid "Changeset status: %s by %s" | |
2909 | msgstr "Statut de changeset : %s par %s" |
|
2907 | msgstr "Statut de changeset : %s par %s" | |
2910 |
|
2908 | |||
|
2909 | msgid "(No commit message)" | |||
|
2910 | msgstr "(Pas de message de commit)" | |||
|
2911 | ||||
2911 | msgid "Expand commit message" |
|
2912 | msgid "Expand commit message" | |
2912 | msgstr "Développer le message de commit" |
|
2913 | msgstr "Développer le message de commit" | |
2913 |
|
2914 | |||
@@ -3070,6 +3071,12 b' msgstr "Afficher le diff complet pour ce' | |||||
3070 | msgid "Show full side-by-side diff for this file" |
|
3071 | msgid "Show full side-by-side diff for this file" | |
3071 | msgstr "Afficher le diff complet côte-à-côte pour ce fichier" |
|
3072 | msgstr "Afficher le diff complet côte-à-côte pour ce fichier" | |
3072 |
|
3073 | |||
|
3074 | msgid "Raw diff for this file" | |||
|
3075 | msgstr "Diff brut pour ce fichier" | |||
|
3076 | ||||
|
3077 | msgid "Download diff for this file" | |||
|
3078 | msgstr "Télécharger le diff pour ce fichier" | |||
|
3079 | ||||
3073 | msgid "Show inline comments" |
|
3080 | msgid "Show inline comments" | |
3074 | msgstr "Afficher les commentaires de ligne" |
|
3081 | msgstr "Afficher les commentaires de ligne" | |
3075 |
|
3082 | |||
@@ -3243,6 +3250,9 b' msgstr "Diff c\xc3\xb4te-\xc3\xa0-c\xc3\xb4te de fichier pour %s"' | |||||
3243 | msgid "File diff" |
|
3250 | msgid "File diff" | |
3244 | msgstr "Diff de fichier" |
|
3251 | msgstr "Diff de fichier" | |
3245 |
|
3252 | |||
|
3253 | msgid "Ignore whitespace" | |||
|
3254 | msgstr "Ignorer les espaces et tabulations" | |||
|
3255 | ||||
3246 | msgid "%s File Diff" |
|
3256 | msgid "%s File Diff" | |
3247 | msgstr "Diff de fichier pour %s" |
|
3257 | msgstr "Diff de fichier pour %s" | |
3248 |
|
3258 |
@@ -10,6 +10,9 b' msgstr ""' | |||||
10 | "Content-Transfer-Encoding: 8bit\n" |
|
10 | "Content-Transfer-Encoding: 8bit\n" | |
11 | "Plural-Forms: nplurals=1; plural=0;\n" |
|
11 | "Plural-Forms: nplurals=1; plural=0;\n" | |
12 |
|
12 | |||
|
13 | msgid "Repository not found in the filesystem" | |||
|
14 | msgstr "ファイルシステム内にリポジトリが見つかりません" | |||
|
15 | ||||
13 | msgid "There are no changesets yet" |
|
16 | msgid "There are no changesets yet" | |
14 | msgstr "まだチェンジセットがありません" |
|
17 | msgstr "まだチェンジセットがありません" | |
15 |
|
18 | |||
@@ -19,15 +22,6 b' msgstr "\xe3\x81\xaa\xe3\x81\x97"' | |||||
19 | msgid "(closed)" |
|
22 | msgid "(closed)" | |
20 | msgstr "(閉鎖済み)" |
|
23 | msgstr "(閉鎖済み)" | |
21 |
|
24 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "空白を表示" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "空白を無視" |
|
|||
27 |
|
||||
28 | msgid "Increase diff context to %(num)s lines" |
|
|||
29 | msgstr "diff コンテキストを %(num)s 行増やす" |
|
|||
30 |
|
||||
31 | msgid "Such revision does not exist for this repository" |
|
25 | msgid "Such revision does not exist for this repository" | |
32 | msgstr "お探しのリビジョンはこのリポジトリにはありません" |
|
26 | msgstr "お探しのリビジョンはこのリポジトリにはありません" | |
33 |
|
27 | |||
@@ -435,13 +429,6 b' msgstr "\xe3\x83\xaa\xe3\x83\x9d\xe3\x82\xb8\xe3\x83\x88\xe3\x83\xaa\xe3\x82\xb9\xe3\x83\x86\xe3\x83\xbc\xe3\x83\x88\xe3\x81\xae\xe5\x89\x8a\xe9\x99\xa4\xe4\xb8\xad\xe3\x81\xab\xe3\x82\xa8\xe3\x83\xa9\xe3\x83\xbc\xe3\x81\x8c\xe7\x99\xba\xe7\x94\x9f\xe3\x81\x97\xe3\x81\xbe\xe3\x81\x97\xe3\x81\x9f"' | |||||
435 | msgid "Updated VCS settings" |
|
429 | msgid "Updated VCS settings" | |
436 | msgstr "VCS設定を更新しました" |
|
430 | msgstr "VCS設定を更新しました" | |
437 |
|
431 | |||
438 | msgid "" |
|
|||
439 | "Unable to activate hgsubversion support. The \"hgsubversion\" library is " |
|
|||
440 | "missing" |
|
|||
441 | msgstr "" |
|
|||
442 | "\"hgsubversion\"ライブラリが見つからないため、hgsubversionサポートを有効に" |
|
|||
443 | "出来ません" |
|
|||
444 |
|
||||
445 | msgid "Error occurred while updating application settings" |
|
432 | msgid "Error occurred while updating application settings" | |
446 | msgstr "アプリケーション設定の更新中にエラーが発生しました" |
|
433 | msgstr "アプリケーション設定の更新中にエラーが発生しました" | |
447 |
|
434 | |||
@@ -539,9 +526,6 b' msgstr ""' | |||||
539 | msgid "You need to be signed in to view this page" |
|
526 | msgid "You need to be signed in to view this page" | |
540 | msgstr "このページを閲覧するためにはサインインが必要です" |
|
527 | msgstr "このページを閲覧するためにはサインインが必要です" | |
541 |
|
528 | |||
542 | msgid "Repository not found in the filesystem" |
|
|||
543 | msgstr "ファイルシステム内にリポジトリが見つかりません" |
|
|||
544 |
|
||||
545 | msgid "Binary file" |
|
529 | msgid "Binary file" | |
546 | msgstr "バイナリファイル" |
|
530 | msgstr "バイナリファイル" | |
547 |
|
531 | |||
@@ -554,6 +538,9 b' msgstr ""' | |||||
554 | msgid "No changes detected" |
|
538 | msgid "No changes detected" | |
555 | msgstr "検出された変更はありません" |
|
539 | msgstr "検出された変更はありません" | |
556 |
|
540 | |||
|
541 | msgid "Increase diff context to %(num)s lines" | |||
|
542 | msgstr "diff コンテキストを %(num)s 行増やす" | |||
|
543 | ||||
557 | msgid "Deleted branch: %s" |
|
544 | msgid "Deleted branch: %s" | |
558 | msgstr "削除されたブランチ: %s" |
|
545 | msgstr "削除されたブランチ: %s" | |
559 |
|
546 | |||
@@ -662,15 +649,6 b' msgstr "\xe3\x83\xaa\xe3\x83\x8d\xe3\x83\xbc\xe3\x83\xa0"' | |||||
662 | msgid "chmod" |
|
649 | msgid "chmod" | |
663 | msgstr "chmod" |
|
650 | msgstr "chmod" | |
664 |
|
651 | |||
665 | msgid "" |
|
|||
666 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
667 | "the filesystem please run the application again in order to rescan " |
|
|||
668 | "repositories" |
|
|||
669 | msgstr "" |
|
|||
670 | "%s リポジトリはDB内に見つかりませんでした。おそらくファイルシステム上で作" |
|
|||
671 | "られたか名前が変更されたためです。リポジトリをもう一度チェックするためにア" |
|
|||
672 | "プリケーションを再起動してください" |
|
|||
673 |
|
||||
674 | msgid "%d year" |
|
652 | msgid "%d year" | |
675 | msgid_plural "%d years" |
|
653 | msgid_plural "%d years" | |
676 | msgstr[0] "%d 年" |
|
654 | msgstr[0] "%d 年" | |
@@ -710,12 +688,6 b' msgstr "%s \xe3\x81\xa8 %s \xe5\x89\x8d"' | |||||
710 | msgid "just now" |
|
688 | msgid "just now" | |
711 | msgstr "たったいま" |
|
689 | msgstr "たったいま" | |
712 |
|
690 | |||
713 | msgid "on line %s" |
|
|||
714 | msgstr "%s 行目" |
|
|||
715 |
|
||||
716 | msgid "[Mention]" |
|
|||
717 | msgstr "[Mention]" |
|
|||
718 |
|
||||
719 | msgid "top level" |
|
691 | msgid "top level" | |
720 | msgstr "top level" |
|
692 | msgstr "top level" | |
721 |
|
693 | |||
@@ -733,12 +705,6 b' msgid "Default user has write access to ' | |||||
733 | msgstr "" |
|
705 | msgstr "" | |
734 | "デフォルトユーザーは新しいリポジトリに書き込みアクセスする権限があります" |
|
706 | "デフォルトユーザーは新しいリポジトリに書き込みアクセスする権限があります" | |
735 |
|
707 | |||
736 | msgid "Only admins can create repository groups" |
|
|||
737 | msgstr "管理者のみがリポジトリのグループを作成できます" |
|
|||
738 |
|
||||
739 | msgid "Non-admins can create repository groups" |
|
|||
740 | msgstr "非管理者がリポジトリのグループを作成できます" |
|
|||
741 |
|
||||
742 | msgid "Only admins can create user groups" |
|
708 | msgid "Only admins can create user groups" | |
743 | msgstr "管理者だけがユーザー グループを作成することができます" |
|
709 | msgstr "管理者だけがユーザー グループを作成することができます" | |
744 |
|
710 | |||
@@ -751,16 +717,6 b' msgstr "\xe7\xae\xa1\xe7\x90\x86\xe8\x80\x85\xe3\x81\xa0\xe3\x81\x91\xe3\x81\x8c\xe3\x83\x88\xe3\x83\x83\xe3\x83\x97\xe3\x83\xac\xe3\x83\x99\xe3\x83\xab\xe3\x81\xab\xe3\x83\xaa\xe3\x83\x9d\xe3\x82\xb8\xe3\x83\x88\xe3\x83\xaa\xe3\x82\x92\xe4\xbd\x9c\xe6\x88\x90\xe3\x81\x99\xe3\x82\x8b\xe3\x81\x93\xe3\x81\xa8\xe3\x81\x8c\xe3\x81\xa7\xe3\x81\x8d\xe3\x81\xbe\xe3\x81\x99"' | |||||
751 | msgid "Non-admins can create top level repositories" |
|
717 | msgid "Non-admins can create top level repositories" | |
752 | msgstr "非管理者がトップレベルにリポジトリを作成することができます" |
|
718 | msgstr "非管理者がトップレベルにリポジトリを作成することができます" | |
753 |
|
719 | |||
754 | msgid "" |
|
|||
755 | "Repository creation enabled with write permission to a repository group" |
|
|||
756 | msgstr "" |
|
|||
757 | "リポジトリグループの書き込みパーミッションを使ったリポジトリ作成が有効です" |
|
|||
758 |
|
||||
759 | msgid "" |
|
|||
760 | "Repository creation disabled with write permission to a repository group" |
|
|||
761 | msgstr "" |
|
|||
762 | "リポジトリグループの書き込みパーミッションを使ったリポジトリ作成は無効です" |
|
|||
763 |
|
||||
764 | msgid "Only admins can fork repositories" |
|
720 | msgid "Only admins can fork repositories" | |
765 | msgstr "管理者のみがリポジトリをフォークすることができます" |
|
721 | msgstr "管理者のみがリポジトリをフォークすることができます" | |
766 |
|
722 | |||
@@ -803,18 +759,9 b' msgstr "\xe6\x96\xb0\xe3\x81\x97\xe3\x81\x84\xe3\x83\xa6\xe3\x83\xbc\xe3\x82\xb6\xe3\x83\xbc %(new_username)s \xe3\x81\x8c\xe7\x99\xbb\xe9\x8c\xb2\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xbe\xe3\x81\x97\xe3\x81\x9f"' | |||||
803 | msgid "Closing" |
|
759 | msgid "Closing" | |
804 | msgstr "クローズ" |
|
760 | msgstr "クローズ" | |
805 |
|
761 | |||
806 | msgid "" |
|
|||
807 | "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s" |
|
|||
808 | msgstr "" |
|
|||
809 | "%(user)s がプリリクエスト #%(pr_nice_id)s: %(pr_title)s のレビューを求めて" |
|
|||
810 | "います" |
|
|||
811 |
|
||||
812 | msgid "latest tip" |
|
762 | msgid "latest tip" | |
813 | msgstr "最新のtip" |
|
763 | msgstr "最新のtip" | |
814 |
|
764 | |||
815 | msgid "New user registration" |
|
|||
816 | msgstr "新規ユーザー登録" |
|
|||
817 |
|
||||
818 | msgid "" |
|
765 | msgid "" | |
819 | "User \"%s\" still owns %s repositories and cannot be removed. Switch " |
|
766 | "User \"%s\" still owns %s repositories and cannot be removed. Switch " | |
820 | "owners or remove those repositories: %s" |
|
767 | "owners or remove those repositories: %s" | |
@@ -1007,10 +954,10 b' msgstr "\xe3\x83\xa6\xe3\x83\xbc\xe3\x82\xb6\xe3\x83\xbc\xe5\x90\x8d"' | |||||
1007 | msgid "Password" |
|
954 | msgid "Password" | |
1008 | msgstr "パスワード" |
|
955 | msgstr "パスワード" | |
1009 |
|
956 | |||
1010 |
msgid "Forgot your password |
|
957 | msgid "Forgot your password?" | |
1011 | msgstr "パスワードを忘れた?" |
|
958 | msgstr "パスワードを忘れた?" | |
1012 |
|
959 | |||
1013 |
msgid "Don't have an account |
|
960 | msgid "Don't have an account?" | |
1014 | msgstr "アカウントを持っていない?" |
|
961 | msgstr "アカウントを持っていない?" | |
1015 |
|
962 | |||
1016 | msgid "Sign In" |
|
963 | msgid "Sign In" | |
@@ -1357,9 +1304,6 b' msgstr "\xe3\x83\xa6\xe3\x83\xbc\xe3\x82\xb6\xe3\x83\xbc\xe3\x82\xb0\xe3\x83\xab\xe3\x83\xbc\xe3\x83\x97"' | |||||
1357 | msgid "Top level repository creation" |
|
1304 | msgid "Top level repository creation" | |
1358 | msgstr "トップレベルリポジトリの作成" |
|
1305 | msgstr "トップレベルリポジトリの作成" | |
1359 |
|
1306 | |||
1360 | msgid "Repository creation with group write access" |
|
|||
1361 | msgstr "グループ書き込み権限でのリポジトリ作成" |
|
|||
1362 |
|
||||
1363 | msgid "User group creation" |
|
1307 | msgid "User group creation" | |
1364 | msgstr "ユーザーグループ作成" |
|
1308 | msgstr "ユーザーグループ作成" | |
1365 |
|
1309 | |||
@@ -1711,12 +1655,6 b' msgstr ""' | |||||
1711 | msgid "Save Settings" |
|
1655 | msgid "Save Settings" | |
1712 | msgstr "設定を保存" |
|
1656 | msgstr "設定を保存" | |
1713 |
|
1657 | |||
1714 | msgid "Built-in Mercurial Hooks (Read-Only)" |
|
|||
1715 | msgstr "組み込みのMercurialフック (編集不可)" |
|
|||
1716 |
|
||||
1717 | msgid "Custom Hooks" |
|
|||
1718 | msgstr "カスタムフック" |
|
|||
1719 |
|
||||
1720 | msgid "" |
|
1658 | msgid "" | |
1721 | "Hooks can be used to trigger actions on certain events such as push / " |
|
1659 | "Hooks can be used to trigger actions on certain events such as push / " | |
1722 | "pull. They can trigger Python functions or external applications." |
|
1660 | "pull. They can trigger Python functions or external applications." | |
@@ -1737,25 +1675,6 b' msgstr "\xe3\x81\x99\xe3\x81\xb9\xe3\x81\xa6\xe3\x81\xae\xe3\x83\xaa\xe3\x83\x9d\xe3\x82\xb8\xe3\x83\x88\xe3\x83\xaa\xe3\x81\xae\xe3\x82\xad\xe3\x83\xa3\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa5\xe3\x82\x92\xe7\x84\xa1\xe5\x8a\xb9\xe5\x8c\x96\xe3\x81\x99\xe3\x82\x8b"' | |||||
1737 | msgid "Install Git hooks" |
|
1675 | msgid "Install Git hooks" | |
1738 | msgstr "Gitフックをインストール" |
|
1676 | msgstr "Gitフックをインストール" | |
1739 |
|
1677 | |||
1740 | msgid "" |
|
|||
1741 | "Verify if Kallithea's Git hooks are installed for each repository. " |
|
|||
1742 | "Current hooks will be updated to the latest version." |
|
|||
1743 | msgstr "" |
|
|||
1744 | "各リポジトリに Kallitheas の Gitフックがインストールされているか確認してく" |
|
|||
1745 | "ださい。現在のフックは最新版に更新されます" |
|
|||
1746 |
|
||||
1747 | msgid "Overwrite existing Git hooks" |
|
|||
1748 | msgstr "既存のGitフックを上書きする" |
|
|||
1749 |
|
||||
1750 | msgid "" |
|
|||
1751 | "If installing Git hooks, overwrite any existing hooks, even if they do " |
|
|||
1752 | "not seem to come from Kallithea. WARNING: This operation will destroy any " |
|
|||
1753 | "custom git hooks you may have deployed by hand!" |
|
|||
1754 | msgstr "" |
|
|||
1755 | "GitフックをインストールするとKallitheaから設定されたものであっても既存の" |
|
|||
1756 | "フックは全て上書きされます。警告: この操作はあなたが手動で配置したGitのカ" |
|
|||
1757 | "スタムフックを全て破壊します!" |
|
|||
1758 |
|
||||
1759 | msgid "Rescan Repositories" |
|
1678 | msgid "Rescan Repositories" | |
1760 | msgstr "リポジトリを再スキャン" |
|
1679 | msgstr "リポジトリを再スキャン" | |
1761 |
|
1680 | |||
@@ -1811,16 +1730,6 b' msgstr "Mercurial\xe3\x82\xa8\xe3\x82\xaf\xe3\x82\xb9\xe3\x83\x86\xe3\x83\xb3\xe3\x82\xb7\xe3\x83\xa7\xe3\x83\xb3"' | |||||
1811 | msgid "Enable largefiles extension" |
|
1730 | msgid "Enable largefiles extension" | |
1812 | msgstr "largefilesエクステンションを有効にする" |
|
1731 | msgstr "largefilesエクステンションを有効にする" | |
1813 |
|
1732 | |||
1814 | msgid "Enable hgsubversion extension" |
|
|||
1815 | msgstr "hgsubversionエクステンションを有効にする" |
|
|||
1816 |
|
||||
1817 | msgid "" |
|
|||
1818 | "Requires hgsubversion library to be installed. Enables cloning of remote " |
|
|||
1819 | "Subversion repositories while converting them to Mercurial." |
|
|||
1820 | msgstr "" |
|
|||
1821 | "hgsubversion ライブラリのインストールが必要です。リモートのSVNリポジトリを" |
|
|||
1822 | "クローンしてMercurialリポジトリに変換するすることが可能です。" |
|
|||
1823 |
|
||||
1824 | msgid "Location of repositories" |
|
1733 | msgid "Location of repositories" | |
1825 | msgstr "リポジトリの場所" |
|
1734 | msgstr "リポジトリの場所" | |
1826 |
|
1735 | |||
@@ -2166,7 +2075,7 b' msgstr "\xe3\x83\xaa\xe3\x83\x93\xe3\x82\xb8\xe3\x83\xa7\xe3\x83\xb3\xe3\x81\xaa\xe3\x81\x97"' | |||||
2166 | msgid "Failed to revoke permission" |
|
2075 | msgid "Failed to revoke permission" | |
2167 | msgstr "権限の取消に失敗しました" |
|
2076 | msgstr "権限の取消に失敗しました" | |
2168 |
|
2077 | |||
2169 |
msgid "Confirm to revoke permission for {0}: {1} |
|
2078 | msgid "Confirm to revoke permission for {0}: {1}?" | |
2170 | msgstr "権限 {0}: {1} を取り消してもよろしいですか?" |
|
2079 | msgstr "権限 {0}: {1} を取り消してもよろしいですか?" | |
2171 |
|
2080 | |||
2172 | msgid "Select changeset" |
|
2081 | msgid "Select changeset" | |
@@ -2408,6 +2317,9 b' msgstr "%s \xe3\x83\x95\xe3\x82\xa1\xe3\x82\xa4\xe3\x83\xab\xe3\x81\xae\xe5\xb7\xae\xe5\x88\x86\xe3\x82\x92\xe4\xb8\xa6\xe3\x81\xb9\xe3\x81\xa6\xe8\xa1\xa8\xe7\xa4\xba"' | |||||
2408 | msgid "File diff" |
|
2317 | msgid "File diff" | |
2409 | msgstr "ファイル差分" |
|
2318 | msgstr "ファイル差分" | |
2410 |
|
2319 | |||
|
2320 | msgid "Ignore whitespace" | |||
|
2321 | msgstr "空白を無視" | |||
|
2322 | ||||
2411 | msgid "%s File Diff" |
|
2323 | msgid "%s File Diff" | |
2412 | msgstr "%s ファイル差分" |
|
2324 | msgstr "%s ファイル差分" | |
2413 |
|
2325 |
@@ -13,18 +13,15 b' msgstr ""' | |||||
13 | msgid "There are no changesets yet" |
|
13 | msgid "There are no changesets yet" | |
14 | msgstr "Et sinn nach keng Ännerungen do" |
|
14 | msgstr "Et sinn nach keng Ännerungen do" | |
15 |
|
15 | |||
|
16 | msgid "SSH access is disabled." | |||
|
17 | msgstr "SSH Accès ass ausgeschalt." | |||
|
18 | ||||
16 | msgid "None" |
|
19 | msgid "None" | |
17 | msgstr "Keng" |
|
20 | msgstr "Keng" | |
18 |
|
21 | |||
19 | msgid "(closed)" |
|
22 | msgid "(closed)" | |
20 | msgstr "(Zou)" |
|
23 | msgstr "(Zou)" | |
21 |
|
24 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Leerzeechen uweisen" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Leerzechen ignoréieren" |
|
|||
27 |
|
||||
28 | msgid "No permission to change status" |
|
25 | msgid "No permission to change status" | |
29 | msgstr "Keng Erlabnis fir den Status ze änneren" |
|
26 | msgstr "Keng Erlabnis fir den Status ze änneren" | |
30 |
|
27 | |||
@@ -163,9 +160,6 b' msgstr "N\xc3\xa4ischt"' | |||||
163 | msgid "Please enter email address" |
|
160 | msgid "Please enter email address" | |
164 | msgstr "Wannechgelift E-Mail-Adress afügen" |
|
161 | msgstr "Wannechgelift E-Mail-Adress afügen" | |
165 |
|
162 | |||
166 | msgid "SSH access is disabled." |
|
|||
167 | msgstr "SSH Accès ass ausgeschalt." |
|
|||
168 |
|
||||
169 | msgid "Binary file" |
|
163 | msgid "Binary file" | |
170 | msgstr "Binär Datei" |
|
164 | msgstr "Binär Datei" | |
171 |
|
165 | |||
@@ -214,6 +208,9 b' msgstr "Keng \xc3\x84nnerungen"' | |||||
214 | msgid "No changesets yet" |
|
208 | msgid "No changesets yet" | |
215 | msgstr "Nach keng Ännerungen do" |
|
209 | msgstr "Nach keng Ännerungen do" | |
216 |
|
210 | |||
|
211 | msgid "Ignore whitespace" | |||
|
212 | msgstr "Leerzechen ignoréieren" | |||
|
213 | ||||
217 | msgid "There are no forks yet" |
|
214 | msgid "There are no forks yet" | |
218 | msgstr "Et sinn nach keng Ofzweigungen do" |
|
215 | msgstr "Et sinn nach keng Ofzweigungen do" | |
219 |
|
216 |
@@ -19,15 +19,6 b' msgstr "Ingen"' | |||||
19 | msgid "(closed)" |
|
19 | msgid "(closed)" | |
20 | msgstr "(lukket)" |
|
20 | msgstr "(lukket)" | |
21 |
|
21 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Vis blanktegn" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Ignorer blanktegn" |
|
|||
27 |
|
||||
28 | msgid "Increase diff context to %(num)s lines" |
|
|||
29 | msgstr "Øk diff-bindeleddsinformasjon til %(num)s linjer" |
|
|||
30 |
|
||||
31 | msgid "Successfully deleted pull request %s" |
|
22 | msgid "Successfully deleted pull request %s" | |
32 | msgstr "Slettet flettingsforespørsel %s" |
|
23 | msgstr "Slettet flettingsforespørsel %s" | |
33 |
|
24 | |||
@@ -436,6 +427,9 b' msgstr "Fjernet IP-adressen fra brukerhv' | |||||
436 | msgid "Binary file" |
|
427 | msgid "Binary file" | |
437 | msgstr "Binærfil" |
|
428 | msgstr "Binærfil" | |
438 |
|
429 | |||
|
430 | msgid "Increase diff context to %(num)s lines" | |||
|
431 | msgstr "Øk diff-bindeleddsinformasjon til %(num)s linjer" | |||
|
432 | ||||
439 | msgid "Fork name %s" |
|
433 | msgid "Fork name %s" | |
440 | msgstr "Forgreningsnavn %s" |
|
434 | msgstr "Forgreningsnavn %s" | |
441 |
|
435 | |||
@@ -517,9 +511,6 b' msgstr "%s og %s siden"' | |||||
517 | msgid "just now" |
|
511 | msgid "just now" | |
518 | msgstr "akkurat nå" |
|
512 | msgstr "akkurat nå" | |
519 |
|
513 | |||
520 | msgid "on line %s" |
|
|||
521 | msgstr "på linje %s" |
|
|||
522 |
|
||||
523 | msgid "top level" |
|
514 | msgid "top level" | |
524 | msgstr "toppnivå" |
|
515 | msgstr "toppnivå" | |
525 |
|
516 | |||
@@ -614,9 +605,12 b' msgstr "Brukernavn"' | |||||
614 | msgid "Password" |
|
605 | msgid "Password" | |
615 | msgstr "Passord" |
|
606 | msgstr "Passord" | |
616 |
|
607 | |||
617 |
msgid "Forgot your password |
|
608 | msgid "Forgot your password?" | |
618 | msgstr "Glemt passordet ditt?" |
|
609 | msgstr "Glemt passordet ditt?" | |
619 |
|
610 | |||
|
611 | msgid "Don't have an account?" | |||
|
612 | msgstr "Mangler du konto?" | |||
|
613 | ||||
620 | msgid "Password Reset" |
|
614 | msgid "Password Reset" | |
621 | msgstr "Passordstilbakestilling" |
|
615 | msgstr "Passordstilbakestilling" | |
622 |
|
616 | |||
@@ -1141,9 +1135,6 b' msgstr "Ikke innlogget"' | |||||
1141 | msgid "Forgot password?" |
|
1135 | msgid "Forgot password?" | |
1142 | msgstr "Glemt passordet?" |
|
1136 | msgstr "Glemt passordet?" | |
1143 |
|
1137 | |||
1144 | msgid "Don't have an account?" |
|
|||
1145 | msgstr "Mangler du konto?" |
|
|||
1146 |
|
||||
1147 | msgid "Log Out" |
|
1138 | msgid "Log Out" | |
1148 | msgstr "Logg ut" |
|
1139 | msgstr "Logg ut" | |
1149 |
|
1140 | |||
@@ -1170,3 +1161,6 b' msgstr "Velg endringssett"' | |||||
1170 |
|
1161 | |||
1171 | msgid "%s comments" |
|
1162 | msgid "%s comments" | |
1172 | msgstr "%s kommentarer" |
|
1163 | msgstr "%s kommentarer" | |
|
1164 | ||||
|
1165 | msgid "Ignore whitespace" | |||
|
1166 | msgstr "Ignorer blanktegn" |
@@ -19,12 +19,6 b' msgstr "Geen"' | |||||
19 | msgid "(closed)" |
|
19 | msgid "(closed)" | |
20 | msgstr "(gesloten)" |
|
20 | msgstr "(gesloten)" | |
21 |
|
21 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Toon witruimtes" |
|
|||
24 |
|
||||
25 | msgid "Increase diff context to %(num)s lines" |
|
|||
26 | msgstr "Vergroot de diff context tot %(num)s lijnen" |
|
|||
27 |
|
||||
28 | msgid "No permission to change status" |
|
22 | msgid "No permission to change status" | |
29 | msgstr "Geen toestemming om de status te veranderen" |
|
23 | msgstr "Geen toestemming om de status te veranderen" | |
30 |
|
24 | |||
@@ -203,6 +197,9 b' msgstr "SSH key succesvol verwijderd"' | |||||
203 | msgid "An error occurred during creation of field: %r" |
|
197 | msgid "An error occurred during creation of field: %r" | |
204 | msgstr "Er is een fout opgetreden tijdens het aanmaken van veld: %r" |
|
198 | msgstr "Er is een fout opgetreden tijdens het aanmaken van veld: %r" | |
205 |
|
199 | |||
|
200 | msgid "Increase diff context to %(num)s lines" | |||
|
201 | msgstr "Vergroot de diff context tot %(num)s lijnen" | |||
|
202 | ||||
206 | msgid "Changeset %s not found" |
|
203 | msgid "Changeset %s not found" | |
207 | msgstr "Changeset %s werd niet gevonden" |
|
204 | msgstr "Changeset %s werd niet gevonden" | |
208 |
|
205 |
@@ -11,21 +11,27 b' msgstr ""' | |||||
11 | "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n" |
|
11 | "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n" | |
12 | "%100<10 || n%100>=20) ? 1 : 2;\n" |
|
12 | "%100<10 || n%100>=20) ? 1 : 2;\n" | |
13 |
|
13 | |||
|
14 | msgid "" | |||
|
15 | "CSRF token leak has been detected - all form tokens have been expired" | |||
|
16 | msgstr "" | |||
|
17 | "Wykryto wyciek tokenu CSRF — wszystkie tokeny formularza zostały " | |||
|
18 | "unieważnione" | |||
|
19 | ||||
14 | msgid "There are no changesets yet" |
|
20 | msgid "There are no changesets yet" | |
15 | msgstr "Brak zestawienia zmian" |
|
21 | msgstr "Brak zestawienia zmian" | |
16 |
|
22 | |||
|
23 | msgid "Changeset for %s %s not found in %s" | |||
|
24 | msgstr "Zmiany dla %s %s nie zostały znalezione w %s" | |||
|
25 | ||||
|
26 | msgid "SSH access is disabled." | |||
|
27 | msgstr "Dostęp SSH jest wyłączony." | |||
|
28 | ||||
17 | msgid "None" |
|
29 | msgid "None" | |
18 | msgstr "Brak" |
|
30 | msgstr "Brak" | |
19 |
|
31 | |||
20 | msgid "(closed)" |
|
32 | msgid "(closed)" | |
21 | msgstr "(zamknięty)" |
|
33 | msgstr "(zamknięty)" | |
22 |
|
34 | |||
23 | msgid "Show whitespace" |
|
|||
24 | msgstr "pokazuj spacje" |
|
|||
25 |
|
||||
26 | msgid "Ignore whitespace" |
|
|||
27 | msgstr "Ignoruj pokazywanie spacji" |
|
|||
28 |
|
||||
29 | msgid "Successfully deleted pull request %s" |
|
35 | msgid "Successfully deleted pull request %s" | |
30 | msgstr "" |
|
36 | msgstr "" | |
31 | "Prośba o skasowanie połączenia gałęzi %s została wykonana prawidłowo" |
|
37 | "Prośba o skasowanie połączenia gałęzi %s została wykonana prawidłowo" | |
@@ -170,9 +176,6 b' msgstr "Nieprawid\xc5\x82owy token resetowania has\xc5\x82a"' | |||||
170 | msgid "Successfully updated password" |
|
176 | msgid "Successfully updated password" | |
171 | msgstr "Pomyślnie zaktualizowano hasło" |
|
177 | msgstr "Pomyślnie zaktualizowano hasło" | |
172 |
|
178 | |||
173 | msgid "Invalid reviewer \"%s\" specified" |
|
|||
174 | msgstr "Podano nieprawidłowego recenzenta \"%\"" |
|
|||
175 |
|
||||
176 | msgid "%s (closed)" |
|
179 | msgid "%s (closed)" | |
177 | msgstr "%s (zamknięty)" |
|
180 | msgstr "%s (zamknięty)" | |
178 |
|
181 | |||
@@ -444,12 +447,6 b' msgstr "Wyst\xc4\x85pi\xc5\x82 b\xc5\x82\xc4\x85d podczas usuwania z repozytorium statystyk"' | |||||
444 | msgid "Updated VCS settings" |
|
447 | msgid "Updated VCS settings" | |
445 | msgstr "Aktualizacja ustawień VCS" |
|
448 | msgstr "Aktualizacja ustawień VCS" | |
446 |
|
449 | |||
447 | msgid "" |
|
|||
448 | "Unable to activate hgsubversion support. The \"hgsubversion\" library is " |
|
|||
449 | "missing" |
|
|||
450 | msgstr "" |
|
|||
451 | "Nie można włączyć obsługi hgsubversion. Brak biblioteki „hgsubversion”" |
|
|||
452 |
|
||||
453 | msgid "Error occurred while updating application settings" |
|
450 | msgid "Error occurred while updating application settings" | |
454 | msgstr "Wystąpił błąd podczas aktualizacji ustawień aplikacji" |
|
451 | msgstr "Wystąpił błąd podczas aktualizacji ustawień aplikacji" | |
455 |
|
452 | |||
@@ -551,18 +548,6 b' msgstr "Musisz by\xc4\x87 zarejestrowanym u\xc5\xbcytkownikiem, \xc5\xbceby wykona\xc4\x87 to dzia\xc5\x82anie"' | |||||
551 | msgid "You need to be signed in to view this page" |
|
548 | msgid "You need to be signed in to view this page" | |
552 | msgstr "Musisz być zalogowany, żeby oglądać stronę" |
|
549 | msgstr "Musisz być zalogowany, żeby oglądać stronę" | |
553 |
|
550 | |||
554 | msgid "" |
|
|||
555 | "CSRF token leak has been detected - all form tokens have been expired" |
|
|||
556 | msgstr "" |
|
|||
557 | "Wykryto wyciek tokenu CSRF — wszystkie tokeny formularza zostały " |
|
|||
558 | "unieważnione" |
|
|||
559 |
|
||||
560 | msgid "Changeset for %s %s not found in %s" |
|
|||
561 | msgstr "Zmiany dla %s %s nie zostały znalezione w %s" |
|
|||
562 |
|
||||
563 | msgid "SSH access is disabled." |
|
|||
564 | msgstr "Dostęp SSH jest wyłączony." |
|
|||
565 |
|
||||
566 | msgid "Binary file" |
|
551 | msgid "Binary file" | |
567 | msgstr "Plik binarny" |
|
552 | msgstr "Plik binarny" | |
568 |
|
553 | |||
@@ -683,41 +668,9 b' msgstr "zmie\xc5\x84 nazw\xc4\x99"' | |||||
683 | msgid "chmod" |
|
668 | msgid "chmod" | |
684 | msgstr "chmod" |
|
669 | msgstr "chmod" | |
685 |
|
670 | |||
686 | msgid "" |
|
|||
687 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
688 | "the filesystem please run the application again in order to rescan " |
|
|||
689 | "repositories" |
|
|||
690 | msgstr "" |
|
|||
691 | "%s repozytorium nie jest mapowane do db może zostało utworzone lub " |
|
|||
692 | "zmienione z systemie plików proszę uruchomić aplikację ponownie, aby " |
|
|||
693 | "ponownie przeskanować repozytoria" |
|
|||
694 |
|
||||
695 | msgid "SSH key is missing" |
|
671 | msgid "SSH key is missing" | |
696 | msgstr "Brak klucza SSH" |
|
672 | msgstr "Brak klucza SSH" | |
697 |
|
673 | |||
698 | msgid "" |
|
|||
699 | "Incorrect SSH key - it must have both a key type and a base64 part, like " |
|
|||
700 | "'ssh-rsa ASRNeaZu4FA...xlJp='" |
|
|||
701 | msgstr "" |
|
|||
702 | "Nieprawidłowy klucz SSH - musi mieć zarówno typ, jak i część kodowaną " |
|
|||
703 | "base64, na przykład „ssh-rsa ASRNeaZu4FA ... xlJp=”" |
|
|||
704 |
|
||||
705 | msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'" |
|
|||
706 | msgstr "" |
|
|||
707 | "Nieprawidłowy klucz SSH - musi zaczynać się od 'ssh-(rsa | dss | ed25519)'" |
|
|||
708 |
|
||||
709 | msgid "Incorrect SSH key - unexpected characters in base64 part %r" |
|
|||
710 | msgstr "" |
|
|||
711 | "Nieprawidłowy klucz SSH - nieoczekiwane znaki w części kodowanej base64 %r" |
|
|||
712 |
|
||||
713 | msgid "Incorrect SSH key - failed to decode base64 part %r" |
|
|||
714 | msgstr "Nieprawidłowy klucz SSH - nie udało się zdekodować części base64 %r" |
|
|||
715 |
|
||||
716 | msgid "Incorrect SSH key - base64 part is not %r as claimed but %r" |
|
|||
717 | msgstr "" |
|
|||
718 | "Nieprawidłowy klucz SSH - część kodowana base64 nie jest %r jak podano, " |
|
|||
719 | "ale %r" |
|
|||
720 |
|
||||
721 | msgid "%d year" |
|
674 | msgid "%d year" | |
722 | msgid_plural "%d years" |
|
675 | msgid_plural "%d years" | |
723 | msgstr[0] "%d rok" |
|
676 | msgstr[0] "%d rok" | |
@@ -769,12 +722,6 b' msgstr "%s i %s temu"' | |||||
769 | msgid "just now" |
|
722 | msgid "just now" | |
770 | msgstr "przed chwilą" |
|
723 | msgstr "przed chwilą" | |
771 |
|
724 | |||
772 | msgid "on line %s" |
|
|||
773 | msgstr "widziany %s" |
|
|||
774 |
|
||||
775 | msgid "[Mention]" |
|
|||
776 | msgstr "[Wymieniony]" |
|
|||
777 |
|
||||
778 | msgid "top level" |
|
725 | msgid "top level" | |
779 | msgstr "najwyższy poziom" |
|
726 | msgstr "najwyższy poziom" | |
780 |
|
727 | |||
@@ -822,13 +769,6 b' msgid "Default user has admin access to ' | |||||
822 | msgstr "" |
|
769 | msgstr "" | |
823 | "Domyślny użytkownik ma dostęp administracyjny do nowych grup użytkowników" |
|
770 | "Domyślny użytkownik ma dostęp administracyjny do nowych grup użytkowników" | |
824 |
|
771 | |||
825 | msgid "Only admins can create repository groups" |
|
|||
826 | msgstr "Tylko administratorzy mogą tworzyć grupy repozytoriów" |
|
|||
827 |
|
||||
828 | msgid "Non-admins can create repository groups" |
|
|||
829 | msgstr "" |
|
|||
830 | "Użytkownicy bez uprawnień administratora mogą tworzyć grupy repozytoriów" |
|
|||
831 |
|
||||
832 | msgid "Only admins can create user groups" |
|
772 | msgid "Only admins can create user groups" | |
833 | msgstr "Tylko administratorzy mogą tworzyć grupy użytkowników" |
|
773 | msgstr "Tylko administratorzy mogą tworzyć grupy użytkowników" | |
834 |
|
774 | |||
@@ -880,13 +820,6 b' msgstr "Wpisz %(min)i lub wi\xc4\x99cej znak\xc3\xb3w"' | |||||
880 | msgid "Name must not contain only digits" |
|
820 | msgid "Name must not contain only digits" | |
881 | msgstr "Nazwa nie może zawierać samych cyfr" |
|
821 | msgstr "Nazwa nie może zawierać samych cyfr" | |
882 |
|
822 | |||
883 | msgid "" |
|
|||
884 | "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on " |
|
|||
885 | "%(branch)s" |
|
|||
886 | msgstr "" |
|
|||
887 | "[Komentarz] %(repo_name)s zmiana %(short_id)s \"%(message_short)s\" w " |
|
|||
888 | "%(branch)s" |
|
|||
889 |
|
||||
890 | msgid "New user %(new_username)s registered" |
|
823 | msgid "New user %(new_username)s registered" | |
891 | msgstr "Użytkownik %(new_username)s zarejestrował się" |
|
824 | msgstr "Użytkownik %(new_username)s zarejestrował się" | |
892 |
|
825 | |||
@@ -902,9 +835,6 b' msgstr "Klucz SSH %s jest ju\xc5\xbc u\xc5\xbcywany przez %s"' | |||||
902 | msgid "SSH key with fingerprint %r found" |
|
835 | msgid "SSH key with fingerprint %r found" | |
903 | msgstr "Znaleziono klucz SSH z odciskiem palca %r" |
|
836 | msgstr "Znaleziono klucz SSH z odciskiem palca %r" | |
904 |
|
837 | |||
905 | msgid "New user registration" |
|
|||
906 | msgstr "nowy użytkownik się zarejestrował" |
|
|||
907 |
|
||||
908 | msgid "" |
|
838 | msgid "" | |
909 | "You can't remove this user since it is crucial for the entire application" |
|
839 | "You can't remove this user since it is crucial for the entire application" | |
910 | msgstr "" |
|
840 | msgstr "" | |
@@ -989,13 +919,6 b' msgstr "Grupa repozytori\xc3\xb3w z nazw\xc4\x85 \\"%(repo)s\\" ju\xc5\xbc istnieje"' | |||||
989 | msgid "Invalid repository URL" |
|
919 | msgid "Invalid repository URL" | |
990 | msgstr "Nieprawidłowy adres URL repozytorium" |
|
920 | msgstr "Nieprawidłowy adres URL repozytorium" | |
991 |
|
921 | |||
992 | msgid "" |
|
|||
993 | "Invalid repository URL. It must be a valid http, https, ssh, svn+http or " |
|
|||
994 | "svn+https URL" |
|
|||
995 | msgstr "" |
|
|||
996 | "Nieprawidłowy adres URL repozytorium. Musi to być prawidłowy adres URL " |
|
|||
997 | "typu http, https, ssh, svn + http lub svn + https" |
|
|||
998 |
|
||||
999 | msgid "Fork has to be the same type as parent" |
|
922 | msgid "Fork has to be the same type as parent" | |
1000 | msgstr "Fork musi być tego samego typu, jak rodzic" |
|
923 | msgstr "Fork musi być tego samego typu, jak rodzic" | |
1001 |
|
924 | |||
@@ -1088,10 +1011,10 b' msgstr "Has\xc5\x82o"' | |||||
1088 | msgid "Stay logged in after browser restart" |
|
1011 | msgid "Stay logged in after browser restart" | |
1089 | msgstr "Pozostań zalogowany po ponownym uruchomieniu przeglądarki" |
|
1012 | msgstr "Pozostań zalogowany po ponownym uruchomieniu przeglądarki" | |
1090 |
|
1013 | |||
1091 |
msgid "Forgot your password |
|
1014 | msgid "Forgot your password?" | |
1092 | msgstr "Zapomniałeś hasła?" |
|
1015 | msgstr "Zapomniałeś hasła?" | |
1093 |
|
1016 | |||
1094 |
msgid "Don't have an account |
|
1017 | msgid "Don't have an account?" | |
1095 | msgstr "Nie masz konta?" |
|
1018 | msgstr "Nie masz konta?" | |
1096 |
|
1019 | |||
1097 | msgid "Sign In" |
|
1020 | msgid "Sign In" | |
@@ -1681,9 +1604,6 b' msgstr "Aktualizacja repozytorium po wys\xc5\x82aniu zmian (aktualizacja hg)"' | |||||
1681 | msgid "Enable largefiles extension" |
|
1604 | msgid "Enable largefiles extension" | |
1682 | msgstr "Rozszerzenia dużych plików" |
|
1605 | msgstr "Rozszerzenia dużych plików" | |
1683 |
|
1606 | |||
1684 | msgid "Enable hgsubversion extension" |
|
|||
1685 | msgstr "Rozszerzenia hgsubversion" |
|
|||
1686 |
|
||||
1687 | msgid "" |
|
1607 | msgid "" | |
1688 | "Click to unlock. You must restart Kallithea in order to make this setting " |
|
1608 | "Click to unlock. You must restart Kallithea in order to make this setting " | |
1689 | "take effect." |
|
1609 | "take effect." | |
@@ -2034,6 +1954,9 b' msgstr "Pliki z list\xc4\x85 zmian i r\xc3\xb3\xc5\xbcnic: %s"' | |||||
2034 | msgid "File diff" |
|
1954 | msgid "File diff" | |
2035 | msgstr "Pliki różnic" |
|
1955 | msgstr "Pliki różnic" | |
2036 |
|
1956 | |||
|
1957 | msgid "Ignore whitespace" | |||
|
1958 | msgstr "Ignoruj pokazywanie spacji" | |||
|
1959 | ||||
2037 | msgid "%s File Diff" |
|
1960 | msgid "%s File Diff" | |
2038 | msgstr "%s Pliki różnic" |
|
1961 | msgstr "%s Pliki różnic" | |
2039 |
|
1962 |
@@ -19,12 +19,6 b' msgstr "Nenhum"' | |||||
19 | msgid "(closed)" |
|
19 | msgid "(closed)" | |
20 | msgstr "(fechado)" |
|
20 | msgstr "(fechado)" | |
21 |
|
21 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Mostrar espaços em branco" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Ignorar espaços em branco" |
|
|||
27 |
|
||||
28 | msgid "" |
|
22 | msgid "" | |
29 | "The request could not be understood by the server due to malformed syntax." |
|
23 | "The request could not be understood by the server due to malformed syntax." | |
30 | msgstr "" |
|
24 | msgstr "" | |
@@ -509,15 +503,6 b' msgstr "renomear"' | |||||
509 | msgid "chmod" |
|
503 | msgid "chmod" | |
510 | msgstr "chmod" |
|
504 | msgstr "chmod" | |
511 |
|
505 | |||
512 | msgid "" |
|
|||
513 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
514 | "the filesystem please run the application again in order to rescan " |
|
|||
515 | "repositories" |
|
|||
516 | msgstr "" |
|
|||
517 | "O repositório %s não está mapeado ao BD. Talvez ele tenha sido criado ou " |
|
|||
518 | "renomeado a partir do sistema de ficheiros. Por favor, execute a " |
|
|||
519 | "aplicação outra vez para varrer novamente por repositórios" |
|
|||
520 |
|
||||
521 | msgid "%d year" |
|
506 | msgid "%d year" | |
522 | msgid_plural "%d years" |
|
507 | msgid_plural "%d years" | |
523 | msgstr[0] "%d ano" |
|
508 | msgstr[0] "%d ano" | |
@@ -563,12 +548,6 b' msgstr "%s e %s atr\xc3\xa1s"' | |||||
563 | msgid "just now" |
|
548 | msgid "just now" | |
564 | msgstr "agora há pouco" |
|
549 | msgstr "agora há pouco" | |
565 |
|
550 | |||
566 | msgid "on line %s" |
|
|||
567 | msgstr "na linha %s" |
|
|||
568 |
|
||||
569 | msgid "[Mention]" |
|
|||
570 | msgstr "[Menção]" |
|
|||
571 |
|
||||
572 | msgid "top level" |
|
551 | msgid "top level" | |
573 | msgstr "nível superior" |
|
552 | msgstr "nível superior" | |
574 |
|
553 | |||
@@ -596,9 +575,6 b' msgstr "Entre com %(min)i caracteres ou ' | |||||
596 | msgid "latest tip" |
|
575 | msgid "latest tip" | |
597 | msgstr "tip mais recente" |
|
576 | msgstr "tip mais recente" | |
598 |
|
577 | |||
599 | msgid "New user registration" |
|
|||
600 | msgstr "Novo registo de utilizador" |
|
|||
601 |
|
||||
602 | msgid "Password reset link" |
|
578 | msgid "Password reset link" | |
603 | msgstr "Ligação para trocar palavra-passe" |
|
579 | msgstr "Ligação para trocar palavra-passe" | |
604 |
|
580 | |||
@@ -718,11 +694,11 b' msgstr "Nome de utilizador"' | |||||
718 | msgid "Password" |
|
694 | msgid "Password" | |
719 | msgstr "Palavra-passe" |
|
695 | msgstr "Palavra-passe" | |
720 |
|
696 | |||
721 |
msgid "Forgot your password |
|
697 | msgid "Forgot your password?" | |
722 |
msgstr "Esqueceu sua palavra-passe |
|
698 | msgstr "Esqueceu sua palavra-passe?" | |
723 |
|
699 | |||
724 |
msgid "Don't have an account |
|
700 | msgid "Don't have an account?" | |
725 |
msgstr "Não possui uma conta |
|
701 | msgstr "Não possui uma conta?" | |
726 |
|
702 | |||
727 | msgid "Sign In" |
|
703 | msgid "Sign In" | |
728 | msgstr "Entrar" |
|
704 | msgstr "Entrar" | |
@@ -1011,9 +987,6 b' msgstr "Atualizar reposit\xc3\xb3rio ap\xc3\xb3s realizar push (hg update)"' | |||||
1011 | msgid "Enable largefiles extension" |
|
987 | msgid "Enable largefiles extension" | |
1012 | msgstr "Ativar extensão largefiles" |
|
988 | msgstr "Ativar extensão largefiles" | |
1013 |
|
989 | |||
1014 | msgid "Enable hgsubversion extension" |
|
|||
1015 | msgstr "Ativar extensão hgsubversion" |
|
|||
1016 |
|
||||
1017 | msgid "" |
|
990 | msgid "" | |
1018 | "Click to unlock. You must restart Kallithea in order to make this setting " |
|
991 | "Click to unlock. You must restart Kallithea in order to make this setting " | |
1019 | "take effect." |
|
992 | "take effect." | |
@@ -1355,6 +1328,9 b' msgstr "Ficheiro %s diff lado-a-lado"' | |||||
1355 | msgid "File diff" |
|
1328 | msgid "File diff" | |
1356 | msgstr "Diff do ficheiro" |
|
1329 | msgstr "Diff do ficheiro" | |
1357 |
|
1330 | |||
|
1331 | msgid "Ignore whitespace" | |||
|
1332 | msgstr "Ignorar espaços em branco" | |||
|
1333 | ||||
1358 | msgid "%s File Diff" |
|
1334 | msgid "%s File Diff" | |
1359 | msgstr "%s Diff de Ficheiro" |
|
1335 | msgstr "%s Diff de Ficheiro" | |
1360 |
|
1336 |
@@ -19,12 +19,6 b' msgstr "Nenhum"' | |||||
19 | msgid "(closed)" |
|
19 | msgid "(closed)" | |
20 | msgstr "(fechado)" |
|
20 | msgstr "(fechado)" | |
21 |
|
21 | |||
22 | msgid "Show whitespace" |
|
|||
23 | msgstr "Mostrar espaços em branco" |
|
|||
24 |
|
||||
25 | msgid "Ignore whitespace" |
|
|||
26 | msgstr "Ignorar espaços em branco" |
|
|||
27 |
|
||||
28 | msgid "" |
|
22 | msgid "" | |
29 | "The request could not be understood by the server due to malformed syntax." |
|
23 | "The request could not be understood by the server due to malformed syntax." | |
30 | msgstr "" |
|
24 | msgstr "" | |
@@ -509,15 +503,6 b' msgstr "renomear"' | |||||
509 | msgid "chmod" |
|
503 | msgid "chmod" | |
510 | msgstr "chmod" |
|
504 | msgstr "chmod" | |
511 |
|
505 | |||
512 | msgid "" |
|
|||
513 | "%s repository is not mapped to db perhaps it was created or renamed from " |
|
|||
514 | "the filesystem please run the application again in order to rescan " |
|
|||
515 | "repositories" |
|
|||
516 | msgstr "" |
|
|||
517 | "O repositório %s não está mapeado ao BD. Talvez ele tenha sido criado ou " |
|
|||
518 | "renomeado a partir do sistema de arquivos. Por favor, execute a aplicação " |
|
|||
519 | "outra vez para varrer novamente por repositórios" |
|
|||
520 |
|
||||
521 | msgid "%d year" |
|
506 | msgid "%d year" | |
522 | msgid_plural "%d years" |
|
507 | msgid_plural "%d years" | |
523 | msgstr[0] "%d ano" |
|
508 | msgstr[0] "%d ano" | |
@@ -563,12 +548,6 b' msgstr "%s e %s atr\xc3\xa1s"' | |||||
563 | msgid "just now" |
|
548 | msgid "just now" | |
564 | msgstr "agora há pouco" |
|
549 | msgstr "agora há pouco" | |
565 |
|
550 | |||
566 | msgid "on line %s" |
|
|||
567 | msgstr "na linha %s" |
|
|||
568 |
|
||||
569 | msgid "[Mention]" |
|
|||
570 | msgstr "[Menção]" |
|
|||
571 |
|
||||
572 | msgid "top level" |
|
551 | msgid "top level" | |
573 | msgstr "nível superior" |
|
552 | msgstr "nível superior" | |
574 |
|
553 | |||
@@ -596,9 +575,6 b' msgstr "Entre com %(min)i caracteres ou ' | |||||
596 | msgid "latest tip" |
|
575 | msgid "latest tip" | |
597 | msgstr "tip mais recente" |
|
576 | msgstr "tip mais recente" | |
598 |
|
577 | |||
599 | msgid "New user registration" |
|
|||
600 | msgstr "Novo registro de usuário" |
|
|||
601 |
|
||||
602 | msgid "Password reset link" |
|
578 | msgid "Password reset link" | |
603 | msgstr "Link para trocar senha" |
|
579 | msgstr "Link para trocar senha" | |
604 |
|
580 | |||
@@ -718,11 +694,11 b' msgstr "Nome de usu\xc3\xa1rio"' | |||||
718 | msgid "Password" |
|
694 | msgid "Password" | |
719 | msgstr "Senha" |
|
695 | msgstr "Senha" | |
720 |
|
696 | |||
721 |
msgid "Forgot your password |
|
697 | msgid "Forgot your password?" | |
722 |
msgstr "Esqueceu sua senha |
|
698 | msgstr "Esqueceu sua senha?" | |
723 |
|
699 | |||
724 |
msgid "Don't have an account |
|
700 | msgid "Don't have an account?" | |
725 |
msgstr "Não possui uma conta |
|
701 | msgstr "Não possui uma conta?" | |
726 |
|
702 | |||
727 | msgid "Sign In" |
|
703 | msgid "Sign In" | |
728 | msgstr "Entrar" |
|
704 | msgstr "Entrar" | |
@@ -1009,9 +985,6 b' msgstr "Atualizar reposit\xc3\xb3rio ap\xc3\xb3s realizar push (hg update)"' | |||||
1009 | msgid "Enable largefiles extension" |
|
985 | msgid "Enable largefiles extension" | |
1010 | msgstr "Habilitar extensão largefiles" |
|
986 | msgstr "Habilitar extensão largefiles" | |
1011 |
|
987 | |||
1012 | msgid "Enable hgsubversion extension" |
|
|||
1013 | msgstr "Habilitar extensão hgsubversion" |
|
|||
1014 |
|
||||
1015 | msgid "" |
|
988 | msgid "" | |
1016 | "Click to unlock. You must restart Kallithea in order to make this setting " |
|
989 | "Click to unlock. You must restart Kallithea in order to make this setting " | |
1017 | "take effect." |
|
990 | "take effect." | |
@@ -1353,6 +1326,9 b' msgstr "Arquivo %s diff lado-a-lado"' | |||||
1353 | msgid "File diff" |
|
1326 | msgid "File diff" | |
1354 | msgstr "Diff do arquivo" |
|
1327 | msgstr "Diff do arquivo" | |
1355 |
|
1328 | |||
|
1329 | msgid "Ignore whitespace" | |||
|
1330 | msgstr "Ignorar espaços em branco" | |||
|
1331 | ||||
1356 | msgid "%s File Diff" |
|
1332 | msgid "%s File Diff" | |
1357 | msgstr "%s Diff de Arquivo" |
|
1333 | msgstr "%s Diff de Arquivo" | |
1358 |
|
1334 |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/lib/celerypylons/__init__.py to kallithea/lib/celery_app.py |
|
NO CONTENT: file renamed from kallithea/lib/celerypylons/__init__.py to kallithea/lib/celery_app.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/config/conf.py to kallithea/lib/conf.py |
|
NO CONTENT: file renamed from kallithea/config/conf.py to kallithea/lib/conf.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/lib/locale.py to kallithea/lib/locales.py |
|
NO CONTENT: file renamed from kallithea/lib/locale.py to kallithea/lib/locales.py |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/lib/vcs/backends/ssh.py to kallithea/lib/vcs/ssh/base.py |
|
NO CONTENT: file renamed from kallithea/lib/vcs/backends/ssh.py to kallithea/lib/vcs/ssh/base.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/lib/vcs/backends/git/ssh.py to kallithea/lib/vcs/ssh/git.py |
|
NO CONTENT: file renamed from kallithea/lib/vcs/backends/git/ssh.py to kallithea/lib/vcs/ssh/git.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/lib/vcs/backends/hg/ssh.py to kallithea/lib/vcs/ssh/hg.py |
|
NO CONTENT: file renamed from kallithea/lib/vcs/backends/hg/ssh.py to kallithea/lib/vcs/ssh/hg.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/lib/celerylib/tasks.py to kallithea/model/async_tasks.py |
|
NO CONTENT: file renamed from kallithea/lib/celerylib/tasks.py to kallithea/model/async_tasks.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/button.html to kallithea/templates/email/button.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/button.html to kallithea/templates/email/button.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/button.txt to kallithea/templates/email/button.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/button.txt to kallithea/templates/email/button.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/changeset_comment.html to kallithea/templates/email/changeset_comment.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/changeset_comment.html to kallithea/templates/email/changeset_comment.html | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/changeset_comment.txt to kallithea/templates/email/changeset_comment.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/changeset_comment.txt to kallithea/templates/email/changeset_comment.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/comment.html to kallithea/templates/email/comment.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/comment.html to kallithea/templates/email/comment.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/comment.txt to kallithea/templates/email/comment.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/comment.txt to kallithea/templates/email/comment.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/default.html to kallithea/templates/email/default.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/default.html to kallithea/templates/email/default.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/default.txt to kallithea/templates/email/default.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/default.txt to kallithea/templates/email/default.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/header.html to kallithea/templates/email/header.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/header.html to kallithea/templates/email/header.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/header.txt to kallithea/templates/email/header.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/header.txt to kallithea/templates/email/header.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/main.html to kallithea/templates/email/main.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/main.html to kallithea/templates/email/main.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/password_reset.html to kallithea/templates/email/password_reset.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/password_reset.html to kallithea/templates/email/password_reset.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/password_reset.txt to kallithea/templates/email/password_reset.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/password_reset.txt to kallithea/templates/email/password_reset.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request.html to kallithea/templates/email/pull_request.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request.html to kallithea/templates/email/pull_request.html | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request.txt to kallithea/templates/email/pull_request.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request.txt to kallithea/templates/email/pull_request.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request_comment.html to kallithea/templates/email/pull_request_comment.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request_comment.html to kallithea/templates/email/pull_request_comment.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request_comment.txt to kallithea/templates/email/pull_request_comment.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/pull_request_comment.txt to kallithea/templates/email/pull_request_comment.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/registration.html to kallithea/templates/email/registration.html |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/registration.html to kallithea/templates/email/registration.html |
1 | NO CONTENT: file renamed from kallithea/templates/email_templates/registration.txt to kallithea/templates/email/registration.txt |
|
NO CONTENT: file renamed from kallithea/templates/email_templates/registration.txt to kallithea/templates/email/registration.txt | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/lib/paster_commands/template.ini.mako to kallithea/templates/ini/template.ini.mako |
|
NO CONTENT: file renamed from kallithea/lib/paster_commands/template.ini.mako to kallithea/templates/ini/template.ini.mako | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/config/rcextensions/__init__.py to kallithea/templates/py/extensions.py |
|
NO CONTENT: file renamed from kallithea/config/rcextensions/__init__.py to kallithea/templates/py/extensions.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file renamed from kallithea/config/post_receive_tmpl.py to kallithea/templates/py/git_post_receive_hook.py |
|
NO CONTENT: file renamed from kallithea/config/post_receive_tmpl.py to kallithea/templates/py/git_post_receive_hook.py | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: modified file |
|
NO CONTENT: modified file | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed | ||
The requested commit or file is too big and content was truncated. Show full diff |
General Comments 0
You need to be logged in to leave comments.
Login now