##// END OF EJS Templates
release: merge default to stable for 0.7.0
Thomas De Schampheleire -
r8688:747cc853 merge stable
parent child Browse files
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 ^rcextensions
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-2020
3 Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2021
4 Mads Kiilerich <mads@kiilerich.com> 2016-2020
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 Private <adamantine.sword@gmail.com> 2019-2020
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_, Mercurial_
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.4
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, < 2.4
6 Sphinx >= 1.8.0, < 3.1
7 WebTest >= 2.0.6, < 2.1
7 WebTest >= 2.0.6, < 2.1
8 isort == 4.3.21
8 isort == 5.1.2
9 pyflakes == 2.1.1
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-2020 by various authors, licensed as GPLv3.'
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 ../kallithea-venv
34 python3 -m venv venv
34 source ../kallithea-venv/bin/activate
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 own Kallithea instance, or patches sent by
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 the same location as all other Python packages you have ever installed. As a
24 Python packages you have ever installed. As a result, removing it is not as
25 result, removing it is not as straightforward as with a virtualenv, as you'd
25 straightforward as with a virtualenv, as you'd have to remove its
26 have to remove its dependencies manually and make sure that they are not
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 ../kallithea-venv
56 python3 -m venv venv
57 . ../kallithea-venv/bin/activate
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 ``virtualenv`` script; it
103 .. note:: You can't use UNIX ``sudo`` to source the ``activate`` script; it
88 will "activate" a shell that terminates immediately. It is also perfectly
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:`setup`.
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 4. **Populate the database.**
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 5. **Configure the web server.**
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 6. **Configure users.**
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.middleware:make_app`` as specified in ``setup.py``.)
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://code.google.com/p/modwsgi/
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.org/en/latest/
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. Create the database by running
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 where
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. After
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 simply execute::
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/en/latest/
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 Hooks can be managed in similar way to that used in ``.hgrc`` files.
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 ``https_fixup = true``, the scheme will be taken from the
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. Make sure you
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:`rcextensions <customization>`, you should also
42 If you are using custom :ref:`extensions <customization>`, you should also
43 make a copy of the entire ``rcextensions`` directory.
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. Update Git repository hooks
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: rcextensions
42 Behavioral customization: Kallithea extensions
43 --------------------------------------
43 ----------------------------------------------
44
44
45 Some behavioral customization can be done in Python using ``rcextensions``, a
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 ``rcextensions`` it's possible to add additional mappings for Whoosh
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 ``rcextensions`` package next to the specified ``ini`` file.
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.org/en/latest/debugging.html#error-middleware>`_
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.org/en/latest/modules/middleware.html#pylons.middleware.ErrorHandler>`_
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 and let them
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 - Each instance's ``data`` storage needs to be configured to be stored on a
70 - Each instance's ``data`` storage needs to be configured to be stored on a
76 shared disk storage, preferably together with repositories. This ``data``
71 shared disk storage, preferably together with repositories. This ``data``
77 dir contains template caches, sessions, whoosh index and is used for
72 dir contains template caches, sessions, whoosh index and is used for
78 task locking (so it is safe across multiple instances). Set the
73 task locking (so it is safe across multiple instances). Set the
79 ``cache_dir``, ``index_dir``, ``beaker.cache.data_dir``, ``beaker.cache.lock_dir``
74 ``cache_dir``, ``index_dir``, ``beaker.cache.data_dir``, ``beaker.cache.lock_dir``
80 variables in each .ini file to a shared location across Kallithea instances
75 variables in each .ini file to a shared location across Kallithea instances
81 - If using several Celery instances,
76 - If using several Celery instances,
82 the message broker should be common to all of them (e.g., one
77 the message broker should be common to all of them (e.g., one
83 shared RabbitMQ server)
78 shared RabbitMQ server)
84 - Load balance using round robin or IP hash, recommended is writing LB rules
79 - Load balance using round robin or IP hash, recommended is writing LB rules
85 that will separate regular user traffic from automated processes like CI
80 that will separate regular user traffic from automated processes like CI
86 servers or build bots.
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 PYTHON_EGG_CACHE="/tmp" start-stop-daemon -d $APP_PATH \
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 -e PYTHON_EGG_CACHE="/tmp" \
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 PYTHON_EGG_CACHE="/tmp" daemon --pidfile $PID_PATH \
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, 3)
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 db
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=db.Base.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.db import Ui
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.db import Ui
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.middleware
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 kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file)
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.middleware.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
82 kallithea.config.application.make_app(config.global_conf, **config.local_conf)
81 return annotated(*args, **kwargs)
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_initialize_app=True)
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.meta import Session
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=kallithea.CONFIG['here'],
63 dbmanage = DbManage(dbconf=dbconf, root=config['here'],
59 tests=False, cli_args=cli_args)
64 cli_args=cli_args)
60 dbmanage.create_tables(override=True)
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_prompt()
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 the ini file. Local customizations in that file will survive upgrades.
36 customizations in that file will survive upgrades. The file contains
38 The file contains instructions on how it can be customized.
37 instructions on how it can be customized.
39 """
38 """
40 here = kallithea.CONFIG['here']
39 here = config['here']
41 content = pkg_resources.resource_string(
40 content = pkg_resources.resource_string(
42 'kallithea', os.path.join('config', 'rcextensions', '__init__.py')
41 'kallithea', os.path.join('templates', 'py', 'extensions.py')
43 )
42 )
44 ext_file = os.path.join(here, 'rcextensions', '__init__.py')
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 = kallithea.CONFIG['__file__']
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_rcextensions
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_rcextensions(kallithea.CONFIG['here'])
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.db import Repository
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 str2bool
24 from kallithea.lib.utils2 import asbool
25 from kallithea.lib.vcs.backends.git.ssh import GitSshHandler
25 from kallithea.lib.vcs.ssh.git import GitSshHandler
26 from kallithea.lib.vcs.backends.hg.ssh import MercurialSshHandler
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 ssh_enabled = kallithea.CONFIG.get('ssh_enabled', False)
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.lib.hooks
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_f, size_root_f, size_total_f = _get_scm_size('.hg', safe_str(repo.root))
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 log_pull_action(ui, repo, **kwargs):
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 log_push_action(ui, repo, node, node_last, **kwargs):
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_log_push_action .
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 kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file)
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.middleware.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
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 handle_git_pre_receive(repo_path, git_stdin_lines):
132 def post_receive(repo_path, git_stdin_lines):
332 """Called from Git pre-receive hook"""
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 baseui, repo = _hook_environment(repo_path)
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-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e))
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 celerypylons
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 # Disable transaction manager -- currently Kallithea takes care of transactions itself
76 # Disable transaction manager -- currently Kallithea takes care of transactions itself
100 self['tm.enabled'] = False
77 'tm.enabled': False,
101
78
102 # Set the default i18n source language so TG doesn't search beyond 'en' in Accept-Language.
79 # Set the default i18n source language so TG doesn't search beyond 'en' in Accept-Language.
103 self['i18n.lang'] = 'en'
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['renderers'].append('kajiki')
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 str2bool(config.get('use_celery')):
138 if asbool(config.get('use_celery')) and not kallithea.CELERY_APP.finalized:
164 kallithea.CELERY_APP = celerypylons.make_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_rcextensions(root_path=config['here'])
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, full_stack=True, **app_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.lib.middleware.https_fixup
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 = str2bool(self.config.get('debug'))
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 str2bool(self.config.get('use_htsts')) and not debug:
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 str2bool(self.config.get('force_https')):
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'] = 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.lib.middleware.permanent_repo_url
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.lib.middleware.pygrack
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.lib.middleware.simplegit
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.lib.hooks import log_pull_action
35 from kallithea.controllers import base
36 from kallithea.lib.middleware.pygrack import make_wsgi_app
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.lib.middleware.simplehg
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.lib.middleware.wrapper
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 _get_ip_addr(environ),
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.db import UserLog
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.config.routing import url
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.base import BaseController, render
38 from kallithea.lib.webutils import url
40 from kallithea.model.db import Setting
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 h.flash(_('Auth settings updated successfully'),
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 h.flash(_('error occurred during update of auth settings'),
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.config.routing import url
37 from kallithea.controllers import base
38 from kallithea.lib import helpers as h
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.base import BaseController, render
40 from kallithea.lib.webutils import url
41 from kallithea.model.db import Setting
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 h.flash(_('Default settings updated successfully'),
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 h.flash(_('Error occurred during update of defaults'),
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.config.routing import url
38 from kallithea.controllers import base
39 from kallithea.lib import helpers as h
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.model.db import Gist
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 h.flash(_('Error occurred during gist creation'), category='error')
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 h.flash(_('Deleted gist %s') % gist.gist_access_id, category='success')
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 h.flash(_('Successfully updated gist content'), category='success')
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 h.flash(_('Successfully updated gist data'), category='success')
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 h.flash(_('Error occurred during update of gist %s') % gist_id,
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.config.routing import url
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 h.flash(_("You can't edit this user since it's"
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['global']
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 h.flash(_('Your account was updated successfully'),
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 h.flash(_('Error occurred during update of user %s')
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 h.flash(_("Successfully updated password"), category='success')
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 h.flash(_('Error occurred during update of user password'),
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 h.flash(_("Added email %s to user") % email, category='success')
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 h.flash(msg, category='error')
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 h.flash(_('An error occurred during email saving'),
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 h.flash(_("Removed email from user"), category='success')
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 h.flash(_("API key successfully created"), category='success')
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 h.flash(_("API key successfully reset"), category='success')
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 h.flash(_("API key successfully deleted"), category='success')
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 h.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success')
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 h.flash(e.args[0], category='error')
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 h.flash(_("SSH key successfully deleted"), category='success')
286 webutils.flash(_("SSH key successfully deleted"), category='success')
294 except SshKeyModelException as e:
287 except SshKeyModelException as e:
295 h.flash(e.args[0], category='error')
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.config.routing import url
39 from kallithea.controllers import base
40 from kallithea.lib import helpers as h
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.base import BaseController, render
42 from kallithea.lib.webutils import url
43 from kallithea.model.db import User, UserIpMap
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 h.flash(_('Global permissions updated successfully'),
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 h.flash(_('Error occurred during update of permissions'),
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.config.routing import url
39 from kallithea.controllers import base
40 from kallithea.lib import helpers as h
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([], 'admin', extras)
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, _=_, h=h, c=c)
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, _=_, h=h, c=c,
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": h.escape(repo_gr.group_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": h.escape(repo_gr.group_description),
121 "desc": webutils.escape(repo_gr.group_description),
123 "repos": repo_count,
122 "repos": repo_count,
124 "owner": h.person(repo_gr.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 h.flash(_('Error occurred during creation of repository group %s')
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 h.flash(_('Created repository group %s') % gr.group_name,
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 h.flash(_('Updated repository group %s')
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 h.flash(_('Error occurred during update of repository group %s')
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 h.flash(_('This group contains %s repositories and cannot be '
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 h.flash(_('This group contains %s subgroups and cannot be deleted'
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 h.flash(_('Removed repository group %s') % group_name,
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 h.flash(_('Error occurred during deletion of repository group %s')
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 h.flash(msg, category='warning')
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 h.flash(_('Repository group permissions updated'), category='success')
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 h.flash(msg, category='warning')
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 h.flash(_('An error occurred during revoking of permission'),
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.config.routing import url
39 from kallithea.controllers import base
41 from kallithea.lib import helpers as h
40 from kallithea.lib import webutils
42 from kallithea.lib.auth import HasPermissionAny, HasRepoPermissionLevelDecorator, LoginRequired, NotAnonymous
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(top_perms, repo_group_perm_level, extras)
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 task = RepoModel().create(form_result, request.authuser.user_id)
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 h.flash(msg, category='error')
124 webutils.flash(msg, category='error')
140 raise HTTPFound(location=url('home'))
125 raise HTTPFound(location=url('home'))
141
126
142 raise HTTPFound(location=h.url('repo_creating_home',
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 task_id=task_id))
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 h.flash(_('Created repository %s from %s')
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 = h.link_to(repo.repo_name,
174 repo_url = webutils.link_to(repo.repo_name,
199 h.url('summary_home',
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 h.flash(h.HTML(_('Forked repository %s as %s'))
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 h.flash(h.HTML(_('Created repository %s')) % repo_url,
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 h.flash(_('Repository %s updated successfully') % repo_name,
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 h.flash(_('Error occurred during update of repository %s')
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 h.flash(_('Detached %s forks') % _forks, category='success')
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 h.flash(_('Deleted %s forks') % _forks, category='success')
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 h.flash(_('Deleted repository %s') % repo_name, category='success')
257 webutils.flash(_('Deleted repository %s') % repo_name, category='success')
283 Session().commit()
258 meta.Session().commit()
284 except AttachedForksError:
259 except AttachedForksError:
285 h.flash(_('Cannot delete repository %s which still has forks')
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 h.flash(_('An error occurred during deletion of %s') % repo_name,
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 h.flash(_('Repository permissions updated'), category='success')
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 h.flash(_('An error occurred during revoking of permission'),
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 h.flash(_('Field validation error: %s') % e.msg, category='error')
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 h.flash(_('An error occurred during creation of field: %r') % e, category='error')
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 h.flash(msg, category='error')
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 h.flash(_('Updated repository visibility in public journal'),
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 h.flash(_('An error occurred during setting this'
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 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
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 h.flash(e, category='error')
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 h.flash(_('An error occurred during this operation'),
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 h.flash(_('Pulled from remote location'), category='success')
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 h.flash(_('An error occurred during pull from remote location'),
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 h.flash(_('An error occurred during deletion of repository stats'),
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', 'hgsubversion')
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 h.flash(_('Error occurred while updating '
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 = h.HTML(', ').join(
150 added_msg = webutils.HTML(', ').join(
172 h.link_to(safe_str(repo_name), h.url('summary_home', repo_name=repo_name)) for repo_name in added
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 = h.HTML(', ').join(
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 h.flash(h.HTML(_('Repositories successfully rescanned. Added: %s. Removed: %s.')) %
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 h.flash(_('Invalidated %s repositories') % i, category='success')
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 h.flash(_('Updated application settings'), category='success')
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 h.flash(_('Error occurred while updating '
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 h.flash(_('Updated visualisation settings'),
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 h.flash(_('Error occurred during updating '
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 h.flash(_('Please enter email address'), category='error')
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 tasks.send_email(recipients, test_email_subj,
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 h.flash(_('Send email task created'), category='success')
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 h.flash(_('Hook already exists'), category='error')
334 webutils.flash(_('Hook already exists'), category='error')
357 elif ui_key in (x.ui_key for x in Ui.get_builtin_hooks()):
335 elif ui_key and '.kallithea_' in ui_key:
358 h.flash(_('Builtin hooks are read-only. Please use another hook name.'), category='error')
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 h.flash(_('Added new hook'), category='success')
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 h.flash(_('Updated hooks'), category='success')
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 h.flash(_('Error occurred during hook creation'),
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_builtin_hooks()
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 tasks.whoosh_index(repo_location, full_index)
381 kallithea.lib.indexers.daemon.whoosh_index(repo_location, full_index)
405 h.flash(_('Whoosh reindex task scheduled'), category='success')
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.lib import helpers as h
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, _=_, h=h, c=c)
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, _=_, h=h, c=c)
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": h.escape(user_gr.user_group_description),
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": h.person(user_gr.owner.username),
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 h.flash(h.HTML(_('Created user group %s')) % h.link_to(gr, url('edit_users_group', id=ug.users_group_id)),
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 h.flash(_('Error occurred during creation of user group %s')
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 h.flash(_('Updated user group %s') % gr, category='success')
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 h.flash(_('Error occurred during update of user group %s')
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 h.flash(_('Successfully deleted user group'), category='success')
206 webutils.flash(_('Successfully deleted user group'), category='success')
209 except UserGroupsAssignedException as e:
207 except UserGroupsAssignedException as e:
210 h.flash(e, category='error')
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 h.flash(_('An error occurred during deletion of user group'),
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 h.flash(_('Target group cannot be the same'), category='error')
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 h.flash(_('User group permissions updated'), category='success')
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 h.flash(msg, category='warning')
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 h.flash(_('An error occurred during revoking of permission'),
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 h.flash(_("Updated permissions"), category='success')
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 h.flash(_('An error occurred during permissions saving'),
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.lib import auth_modules
41 from kallithea.controllers import base
42 from kallithea.lib import helpers as h
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, _=_, h=h, c=c)
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, _=_, h=h, c=c)
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": h.escape(user.name),
89 "firstname": webutils.escape(user.name),
93 "lastname": h.escape(user.lastname),
90 "lastname": webutils.escape(user.lastname),
94 "last_login": h.fmt_date(user.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 h.flash(_('Created user %s') % user.username,
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 h.flash(e, 'error')
130 webutils.flash(e, 'error')
134 except Exception:
131 except Exception:
135 log.error(traceback.format_exc())
132 log.error(traceback.format_exc())
136 h.flash(_('Error occurred during creation of user %s')
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 h.flash(_('User updated successfully'), category='success')
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 h.flash(_('Error occurred during update of user %s')
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 h.flash(_('Successfully deleted user'), category='success')
186 webutils.flash(_('Successfully deleted user'), category='success')
189 except (UserOwnsReposException, DefaultUserException) as e:
187 except (UserOwnsReposException, DefaultUserException) as e:
190 h.flash(e, category='warning')
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 h.flash(_('An error occurred during deletion of user'),
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 h.flash(_("The default user cannot be edited"), category='warning')
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 h.flash(_("API key successfully created"), category='success')
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 h.flash(_("API key successfully reset"), category='success')
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 h.flash(_("API key successfully deleted"), category='success')
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 h.flash(_("Updated permissions"), category='success')
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 h.flash(_('An error occurred during permissions saving'),
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 h.flash(_("Added email %s to user") % email, category='success')
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 h.flash(msg, category='error')
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 h.flash(_('An error occurred during email saving'),
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 h.flash(_("Removed email from user"), category='success')
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 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
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 h.flash(msg, category='error')
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 h.flash(_('An error occurred while adding IP address'),
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 h.flash(_("Removed IP address from user whitelist"), category='success')
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 h.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success')
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 h.flash(e.args[0], category='error')
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 h.flash(_("SSH key successfully deleted"), category='success')
469 webutils.flash(_("SSH key successfully deleted"), category='success')
469 except SshKeyModelException as e:
470 except SshKeyModelException as e:
470 h.flash(e.args[0], category='error')
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.db import User
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 = self._get_ip_addr(environ)
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 self._get_ip_addr(environ),
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 action_logger, repo2db_mapper
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 instance of Optional
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=Optional(None)):
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=Optional.extract(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=Optional(False)):
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 = Optional.extract(remove_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=Optional(OAttr('apiuser'))):
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 isinstance(userid, Optional):
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=Optional(OAttr('apiuser'))):
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 isinstance(userid, Optional) and userid != request.authuser.user_id:
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 isinstance(userid, Optional):
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=Optional(''),
433 def create_user(self, username, email, password='',
436 firstname=Optional(''), lastname=Optional(''),
434 firstname='', lastname='',
437 active=Optional(True), admin=Optional(False),
435 active=True, admin=False,
438 extern_type=Optional(User.DEFAULT_AUTH_TYPE),
436 extern_type=db.User.DEFAULT_AUTH_TYPE,
439 extern_name=Optional('')):
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=Optional.extract(username),
493 username=username,
496 password=Optional.extract(password),
494 password=password,
497 email=Optional.extract(email),
495 email=email,
498 firstname=Optional.extract(firstname),
496 firstname=firstname,
499 lastname=Optional.extract(lastname),
497 lastname=lastname,
500 active=Optional.extract(active),
498 active=active,
501 admin=Optional.extract(admin),
499 admin=admin,
502 extern_type=Optional.extract(extern_type),
500 extern_type=extern_type,
503 extern_name=Optional.extract(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=Optional(None),
513 def update_user(self, userid, username=None,
516 email=Optional(None), password=Optional(None),
514 email=None, password=None,
517 firstname=Optional(None), lastname=Optional(None),
515 firstname=None, lastname=None,
518 active=Optional(None), admin=Optional(None),
516 active=None, admin=None,
519 extern_type=Optional(None), extern_name=Optional(None)):
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=Optional(''),
687 def create_user_group(self, group_name, description='',
690 owner=Optional(OAttr('apiuser')), active=Optional(True)):
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 isinstance(owner, Optional):
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=Optional(''),
744 def update_user_group(self, usergroupid, group_name=None,
749 description=Optional(''), owner=Optional(None),
745 description=None, owner=None,
750 active=Optional(True)):
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=Optional(False),
962 with_revision_names=False,
967 with_pullrequests=Optional(False)):
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=Optional.extract(with_revision_names),
1063 data = repo.get_api_data(with_revision_names=with_revision_names,
1068 with_pullrequests=Optional.extract(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 = RepoModel().get_all_user_repos(user=request.authuser.user_id)
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=Optional('all')):
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=Optional(OAttr('apiuser')),
1166 def create_repo(self, repo_name, owner=None,
1172 repo_type=Optional('hg'), description=Optional(''),
1167 repo_type=None, description='',
1173 private=Optional(False), clone_uri=Optional(None),
1168 private=False, clone_uri=None,
1174 landing_rev=Optional('rev:tip'),
1169 landing_rev='rev:tip',
1175 enable_statistics=Optional(False),
1170 enable_statistics=None,
1176 enable_downloads=Optional(False),
1171 enable_downloads=None,
1177 copy_permissions=Optional(False)):
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 isinstance(owner, Optional):
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 isinstance(owner, Optional):
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 isinstance(private, Optional):
1250 if private is None:
1256 private = defs.get('repo_private') or Optional.extract(private)
1251 private = defs.get('repo_private') or False
1257 if isinstance(repo_type, Optional):
1252 if repo_type is None:
1258 repo_type = defs.get('repo_type')
1253 repo_type = defs.get('repo_type')
1259 if isinstance(enable_statistics, Optional):
1254 if enable_statistics is None:
1260 enable_statistics = defs.get('repo_enable_statistics')
1255 enable_statistics = defs.get('repo_enable_statistics')
1261 if isinstance(enable_downloads, Optional):
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 task = RepoModel().create(form_data=data, cur_user=owner.username)
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=Optional(None),
1287 def update_repo(self, repoid, name=None,
1301 owner=Optional(OAttr('apiuser')),
1288 owner=None,
1302 group=Optional(None),
1289 group=None,
1303 description=Optional(''), private=Optional(False),
1290 description=None, private=None,
1304 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
1291 clone_uri=None, landing_rev=None,
1305 enable_statistics=Optional(False),
1292 enable_statistics=None,
1306 enable_downloads=Optional(False)):
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 isinstance(owner, Optional):
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 isinstance(repo_group, Optional):
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=Optional(OAttr('apiuser')),
1358 owner=None,
1372 description=Optional(''), copy_permissions=Optional(False),
1359 description='', copy_permissions=False,
1373 private=Optional(False), landing_rev=Optional('rev:tip')):
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 isinstance(owner, Optional):
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 isinstance(owner, Optional):
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=Optional.extract(description),
1445 description=description,
1459 private=Optional.extract(private),
1446 private=private,
1460 copy_permissions=Optional.extract(copy_permissions),
1447 copy_permissions=copy_permissions,
1461 landing_rev=Optional.extract(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 task = RepoModel().create_fork(form_data, cur_user=owner.username)
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=Optional('')):
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 = Optional.extract(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=Optional(''),
1768 def create_repo_group(self, group_name, description='',
1784 owner=Optional(OAttr('apiuser')),
1769 owner=None,
1785 parent=Optional(None),
1770 parent=None,
1786 copy_permissions=Optional(False)):
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 isinstance(owner, Optional):
1808 if owner is None:
1824 owner = request.authuser.user_id
1809 owner = request.authuser.user_id
1825 group_description = Optional.extract(description)
1810 group_description = description
1826 parent_group = Optional.extract(parent)
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_group)
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=Optional(''),
1834 def update_repo_group(self, repogroupid, group_name=None,
1851 description=Optional(''),
1835 description=None,
1852 owner=Optional(OAttr('apiuser')),
1836 owner=None,
1853 parent=Optional(None)):
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=Optional('none')):
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=Optional('none')):
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=Optional('none')):
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=Optional('none')):
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=Optional(OAttr('apiuser'))):
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 isinstance(userid, Optional) and userid != request.authuser.user_id:
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 isinstance(userid, Optional):
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=Optional(OAttr('apiuser')),
2204 def create_gist(self, files, owner=None,
2227 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
2205 gist_type=db.Gist.GIST_PUBLIC, lifetime=-1,
2228 description=Optional('')):
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 isinstance(owner, Optional):
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=Optional(False)):
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.lib.base
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, str2bool
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.model import meta
53 from kallithea.lib.webutils import url
55 from kallithea.model.db import PullRequest, Repository, Setting, User
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[0].strip()
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 _get_ip_addr(environ):
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 = self._get_ip_addr(environ)
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 = request.POST.get(h.session_csrf_secret_name)
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 rc_config = Setting.get_app_settings()
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 = str2bool(rc_config.get('show_public_icon'))
367 c.visual.show_public_icon = asbool(settings.get('show_public_icon'))
379 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
368 c.visual.show_private_icon = asbool(settings.get('show_private_icon'))
380 c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels'))
369 c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels'))
381 c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100))
370 c.visual.page_size = safe_int(settings.get('dashboard_items', 100))
382 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
371 c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100))
383 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
372 c.visual.repository_fields = asbool(settings.get('repository_fields'))
384 c.visual.show_version = str2bool(rc_config.get('show_version'))
373 c.visual.show_version = asbool(settings.get('show_version'))
385 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
374 c.visual.use_gravatar = asbool(settings.get('use_gravatar'))
386 c.visual.gravatar_url = rc_config.get('gravatar_url')
375 c.visual.gravatar_url = settings.get('gravatar_url')
387
376
388 c.ga_code = rc_config.get('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 = rc_config.get('title')
391 c.site_name = settings.get('title')
403 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
392 c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI
404 c.clone_ssh_tmpl = rc_config.get('clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH
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 = str2bool(config.get('allow_repo_location_change', True))
396 c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True))
408 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
397 c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True))
409 c.ssh_enabled = str2bool(config.get('ssh_enabled', False))
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(h.session_csrf_secret_name, None)
468 session.pop(webutils.session_csrf_secret_name, None)
482 session.save()
469 session.save()
483 h.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
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 = _get_ip_addr(environ)
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 h.flash(_('There are no changesets yet'), category='error')
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 h.flash(_('Changeset for %s %s not found in %s') %
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 h.flash(e, category='error')
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.config.routing import url
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 h.flash(_('There are no changesets yet'), category='error')
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 h.flash(e, category='error')
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 h.flash(e, category='warning')
114 webutils.flash(e, category='warning')
115 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
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 h.flash(e, category='warning')
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 h.flash(e, category='error')
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, defaultdict
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.lib import diffs
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 h.flash(_('No permission to change status'), 'error')
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 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
99 webutils.flash(_('Successfully deleted pull request %s') % pull_request_id,
214 category='success')
100 category='success')
215 return {
101 return {
216 'location': h.url('my_pullrequests'), # or repo pr list?
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': h.safeid(request.POST.get('f_path')),
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 h.flash(msg, category='error')
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 context_lcl = get_line_ctx('', request.GET)
241 ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET)
361 ign_whitespace_lcl = get_ignore_ws('', request.GET)
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_lcl, context=context_lcl)
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(enable_comments=enable_comments, parsed_lines=[f])
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.changeset import _context_url, _ignorews_url
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.db import Repository
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 h.flash(msg, category='error')
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 h.flash(msg, category='error')
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 = h.url('compare_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 = self._get_changesets(
122 c.cs_ranges, c.cs_ranges_org, c.ancestors = c.a_repo.scm_instance.get_diff_changesets(
229 c.a_repo.scm_instance.alias, c.a_repo.scm_instance, c.a_rev,
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 h.flash(msg, category='error')
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 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
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=line_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(enable_comments=False, parsed_lines=[f])
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.lib import helpers as h
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, str2bool
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 h.shorter(cs.message, 160)
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), h.fmt_date(cs.date))) + '<br/>']
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 inline_diff=False)
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 = h.canonical_url('changeset_home', repo_name=c.db_repo.repo_name,
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(h.urlify_text(cs.message))
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 str2bool(CONFIG.get('rss_include_diff', False)):
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=h.canonical_url('summary_home', repo_name=repo_name),
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=h.canonical_url('changeset_home', repo_name=repo_name, revision=cs.raw_id),
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.lib import diffs
43 from kallithea.controllers import base
44 from kallithea.lib import helpers as h
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.model import db
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 = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
85 add_new = webutils.link_to(_('Click here to add new file'), url_, class_="alert-link")
86 h.flash(_('There are no files yet.') + ' ' + add_new, category='warning')
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 h.flash(msg, category='error')
90 webutils.flash(msg, category='error')
91 raise HTTPNotFound()
91 raise HTTPNotFound()
92 except RepositoryError as e:
92 except RepositoryError as e:
93 h.flash(e, category='error')
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 h.flash(msg, category='error')
110 webutils.flash(msg, category='error')
111 raise HTTPNotFound()
111 raise HTTPNotFound()
112 except RepositoryError as e:
112 except RepositoryError as e:
113 h.flash(e, category='error')
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((h.email(a), h.person(a)))
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 h.flash(e, category='error')
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, h.short_id(c.changeset.raw_id))) for b in c.changeset.branches] + \
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((h.email(a), h.person(a)))
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(db.URL_SEP)[-1]
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 h.flash(_('You can only delete files with revision '
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=h.url('files_home',
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 h.flash(_('Successfully deleted file %s') % f_path,
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 h.flash(_('Error occurred during commit'), category='error')
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 h.flash(_('You can only edit files with revision '
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=h.url('files_home',
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 h.flash(_('No changes'), category='warning')
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 h.flash(_('Successfully committed to %s') % f_path,
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 h.flash(_('Error occurred during commit'), category='error')
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 h.flash(_('No content'), category='warning')
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 h.flash(_('No filename'), category='warning')
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 h.flash(_('Successfully committed to %s') % node_path,
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 h.flash(_('Location must be relative path and must not '
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 h.flash(_(e), category='error')
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 h.flash(_('Error occurred during commit'), category='error')
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 str2bool(request.GET.get('annotate', 'False')):
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=line_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=line_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.wrapped_diff(filenode_old=node1,
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_lcl,
645 ignore_whitespace=ignore_whitespace_diff,
653 line_context=line_context_lcl,
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 h.flash(msg, category='error')
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.db import UserFollowing
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.config.routing import url
40 from kallithea.lib import webutils
41 from kallithea.lib.auth import HasPermissionAny, HasPermissionAnyDecorator, HasRepoPermissionLevel, HasRepoPermissionLevelDecorator, LoginRequired
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.db import Repository, Ui, UserFollowing
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 task = RepoModel().create_fork(form_result, request.authuser.user_id)
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 h.flash(_('An error occurred during repository forking %s') %
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=h.url('repo_creating_home',
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 task_id=task_id))
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 from kallithea.lib import helpers as h
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 = h.canonical_url('changelog_home',
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 h.canonical_url(''),
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 = h.canonical_url('public_journal_atom')
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 = h.canonical_url('journal_atom')
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 = h.canonical_url('public_journal_atom')
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 = h.canonical_url('journal_atom')
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.config.routing import url
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 h.flash(e, 'error')
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 h.flash(_('Authentication failed.'), 'error')
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['global']
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 h.flash(_('You have successfully registered with %s') % (c.site_name or 'Kallithea'),
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 h.flash(e, 'error')
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 h.flash(_('A password reset confirmation code has been sent'),
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 h.flash(_('Successfully updated password'), category='success')
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 h.session_csrf_secret_token()
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 helpers as h
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 h.flash(_('Invalid reviewer "%s" specified') % user_id, category='error')
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 h.is_hg(repo):
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 h.flash(_('There are no changesets yet'),
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 h.flash(msg, 'error')
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 h.flash(e, category='error', logf=log.error)
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 h.flash(_('Error occurred while creating pull request'),
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 h.flash(_('Successfully opened new pull request'),
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 h.flash(e, category='error', logf=log.error)
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 h.flash(_('Error occurred while creating pull request'),
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 h.flash(_('New pull request iteration created'),
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 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
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 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
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 h.flash(_('Pull request updated'), category='success')
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 h.flash(_('Successfully deleted pull request'),
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 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
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 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
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=line_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(enable_comments=True, parsed_lines=[f])
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.config.routing import make_map
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.db import Statistics
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 h.flash(e, category='warning')
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 .line:hover .lineno a {
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: 16px;
177 width: 1.2em;
185 height: 16px;
178 height: 1.2em;
186 line-height: 14px;
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: 3px;
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: 14px;
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 "Incorrect SSH key - it must have both a key type and a base64 part, like "
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 incorrecte – elle doit comporter à la fois un type de clé et une "
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 incorrecte – elle doit commencer par « ssh-(rsa|dss|ed25519) »"
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 "Built-in Mercurial Hooks (Read-Only)"
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