##// END OF EJS Templates
docs/usage: rework section on non-changeable repository URLs and call them 'permanent'
Thomas De Schampheleire -
r4967:4185f87f default
parent child Browse files
Show More
@@ -1,177 +1,178 b''
1 .. _general:
1 .. _general:
2
2
3 =======================
3 =======================
4 General Kallithea usage
4 General Kallithea usage
5 =======================
5 =======================
6
6
7
7
8 Repository deleting
8 Repository deleting
9 -------------------
9 -------------------
10
10
11 Currently when an admin or owner deletes a repository, Kallithea does
11 Currently when an admin or owner deletes a repository, Kallithea does
12 not physically delete said repository from the filesystem, but instead
12 not physically delete said repository from the filesystem, but instead
13 renames it in a special way so that it is not possible to push, clone
13 renames it in a special way so that it is not possible to push, clone
14 or access the repository.
14 or access the repository.
15
15
16 There is a special command for cleaning up such archived repos::
16 There is a special command for cleaning up such archived repos::
17
17
18 paster cleanup-repos --older-than=30d my.ini
18 paster cleanup-repos --older-than=30d my.ini
19
19
20 This command scans for archived repositories that are older than
20 This command scans for archived repositories that are older than
21 30 days, displays them, and asks if you want to delete them (unless given
21 30 days, displays them, and asks if you want to delete them (unless given
22 the ``--dont-ask`` flag). If you host a large amount of repositories with
22 the ``--dont-ask`` flag). If you host a large amount of repositories with
23 forks that are constantly being deleted, it is recommended that you run this
23 forks that are constantly being deleted, it is recommended that you run this
24 command via crontab.
24 command via crontab.
25
25
26 It is worth noting that even if someone is given administrative access to
26 It is worth noting that even if someone is given administrative access to
27 Kallithea and deletes a repository, you can easily restore such an action by
27 Kallithea and deletes a repository, you can easily restore such an action by
28 renaming the repository directory, removing the ``rm__<date>`` prefix.
28 renaming the repository directory, removing the ``rm__<date>`` prefix.
29
29
30 File view: follow current branch
30 File view: follow current branch
31 --------------------------------
31 --------------------------------
32
32
33 In the file view, left and right arrows allow to jump to the previous and next
33 In the file view, left and right arrows allow to jump to the previous and next
34 revision. Depending on the way revisions were created in the repository, this
34 revision. Depending on the way revisions were created in the repository, this
35 could jump to a different branch. When the checkbox ``Follow current branch``
35 could jump to a different branch. When the checkbox ``Follow current branch``
36 is checked, these arrows will only jump to revisions on the same branch as the
36 is checked, these arrows will only jump to revisions on the same branch as the
37 currently visible revision. So for example, if someone is viewing files in the
37 currently visible revision. So for example, if someone is viewing files in the
38 ``beta`` branch and marks the `Follow current branch` checkbox, the < and >
38 ``beta`` branch and marks the `Follow current branch` checkbox, the < and >
39 arrows will only show revisions on the ``beta`` branch.
39 arrows will only show revisions on the ``beta`` branch.
40
40
41
41
42 Changelog features
42 Changelog features
43 ------------------
43 ------------------
44
44
45 The core feature of a repository's ``changelog`` page is to show the revisions
45 The core feature of a repository's ``changelog`` page is to show the revisions
46 in a repository. However, there are several other features available from the
46 in a repository. However, there are several other features available from the
47 changelog.
47 changelog.
48
48
49 Branch filter
49 Branch filter
50 By default, the changelog shows revisions from all branches in the
50 By default, the changelog shows revisions from all branches in the
51 repository. Use the branch filter to restrict to a given branch.
51 repository. Use the branch filter to restrict to a given branch.
52
52
53 Viewing a changeset
53 Viewing a changeset
54 A particular changeset can be opened by clicking on either the changeset
54 A particular changeset can be opened by clicking on either the changeset
55 hash or the commit message, or by ticking the checkbox and clicking the
55 hash or the commit message, or by ticking the checkbox and clicking the
56 ``Show selected changeset`` button at the top.
56 ``Show selected changeset`` button at the top.
57
57
58 Viewing all changes between two changesets
58 Viewing all changes between two changesets
59 To get a list of all changesets between two selected changesets, along with
59 To get a list of all changesets between two selected changesets, along with
60 the changes in each one of them, tick the checkboxes of the first and
60 the changes in each one of them, tick the checkboxes of the first and
61 last changeset in the desired range and click the ``Show selected changesets``
61 last changeset in the desired range and click the ``Show selected changesets``
62 button at the top. You can only show the range between the first and last
62 button at the top. You can only show the range between the first and last
63 checkbox (no cherry-picking).
63 checkbox (no cherry-picking).
64
64
65 From that page, you can proceed to viewing the overall delta between the
65 From that page, you can proceed to viewing the overall delta between the
66 selected changesets, by clicking the ``Compare revisions`` button.
66 selected changesets, by clicking the ``Compare revisions`` button.
67
67
68 Creating a pull request
68 Creating a pull request
69 You can create a new pull request for the changes of a particular changeset
69 You can create a new pull request for the changes of a particular changeset
70 (and its ancestors) by selecting it and clicking the ``Open new pull request
70 (and its ancestors) by selecting it and clicking the ``Open new pull request
71 for selected changesets`` button.
71 for selected changesets`` button.
72
72
73 Non changeable repository urls
73 Permanent repository URLs
74 ------------------------------
74 -------------------------
75
75
76 Due to the complicated nature of repository grouping, URLs of repositories
76 Due to the complicated nature of repository grouping, URLs of repositories
77 can often change.
77 can often change. For example, a repository originally accessible from::
78
79 example::
80
78
81 #before
82 http://server.com/repo_name
79 http://server.com/repo_name
83 # after insertion to test_group group the url will be
80
81 would get a new URL after moving it to test_group::
82
84 http://server.com/test_group/repo_name
83 http://server.com/test_group/repo_name
85
84
86 This can be an issue for build systems and any other hardcoded scripts, moving
85 Such moving of a repository to a group can be an issue for build systems and
87 a repository to a group leads to a need for changing external systems. To
86 other scripts where the repository paths are hardcoded. To mitigate this,
88 overcome this Kallithea introduces a non-changable replacement URL. It's
87 Kallithea provides permanent URLs using the repository ID prefixed with an
89 simply a repository ID prefixed with ``_``. The above URLs are also accessible as::
88 underscore. In all Kallithea URLs, for example those for the changelog and the
89 file view, a repository name can be replaced by this ``_ID`` string. Since IDs
90 are always the same, moving the repository to a different group will not affect
91 such URLs.
92
93 In the example, the repository could also be accessible as::
90
94
91 http://server.com/_<ID>
95 http://server.com/_<ID>
92
96
93 Since IDs are always the same, moving the repository will not affect
97 The ID of a given repository can be shown from the repository ``Summary`` page,
94 such a URL. the ``_<ID>`` syntax can be used anywhere in the system so
98 by selecting the ``Show by ID`` button next to ``Clone URL``.
95 URLs with ``repo_name`` for changelogs and files can be exchanged
96 with the ``_<ID>`` syntax.
97
98
99
99 E-mail notifications
100 E-mail notifications
100 --------------------
101 --------------------
101
102
102 When the administrator correctly specified the e-mail settings in the Kallithea
103 When the administrator correctly specified the e-mail settings in the Kallithea
103 configuration file, Kallithea will send e-mails on user registration and when
104 configuration file, Kallithea will send e-mails on user registration and when
104 errors occur.
105 errors occur.
105
106
106 Mails are also sent for comments on changesets. In this case, an e-mail is sent
107 Mails are also sent for comments on changesets. In this case, an e-mail is sent
107 to the committer of the changeset (if known to Kallithea), to all reviewers of
108 to the committer of the changeset (if known to Kallithea), to all reviewers of
108 the pull request (if applicable) and to all people mentioned in the comment
109 the pull request (if applicable) and to all people mentioned in the comment
109 using @mention notation.
110 using @mention notation.
110
111
111
112
112 Trending source files
113 Trending source files
113 ---------------------
114 ---------------------
114
115
115 Trending source files are calculated based on a pre-defined dict of known
116 Trending source files are calculated based on a pre-defined dict of known
116 types and extensions. If you miss some extension or would like to scan some
117 types and extensions. If you miss some extension or would like to scan some
117 custom files, it is possible to add new types in the ``LANGUAGES_EXTENSIONS_MAP`` dict
118 custom files, it is possible to add new types in the ``LANGUAGES_EXTENSIONS_MAP`` dict
118 located in ``kallithea/lib/celerylib/tasks.py``.
119 located in ``kallithea/lib/celerylib/tasks.py``.
119
120
120
121
121 Cloning remote repositories
122 Cloning remote repositories
122 ---------------------------
123 ---------------------------
123
124
124 Kallithea has the ability to clone remote repos from given remote locations.
125 Kallithea has the ability to clone remote repos from given remote locations.
125 Currently it supports the following options:
126 Currently it supports the following options:
126
127
127 - hg -> hg clone
128 - hg -> hg clone
128 - svn -> hg clone
129 - svn -> hg clone
129 - git -> git clone
130 - git -> git clone
130
131
131
132
132 .. note:: svn -> hg cloning requires tge ``hgsubversion`` library to be installed.
133 .. note:: svn -> hg cloning requires tge ``hgsubversion`` library to be installed.
133
134
134 If you need to clone repositories that are protected via basic auth, you
135 If you need to clone repositories that are protected via basic auth, you
135 might pass the url with stored credentials inside, e.g.,
136 might pass the url with stored credentials inside, e.g.,
136 ``http://user:passw@remote.server/repo``, Kallithea will try to login and clone
137 ``http://user:passw@remote.server/repo``, Kallithea will try to login and clone
137 using the given credentials. Please take note that they will be stored as
138 using the given credentials. Please take note that they will be stored as
138 plaintext inside the database. Kallithea will remove auth info when showing the
139 plaintext inside the database. Kallithea will remove auth info when showing the
139 clone url in summary page.
140 clone url in summary page.
140
141
141
142
142
143
143 Specific features configurable in the Admin settings
144 Specific features configurable in the Admin settings
144 ----------------------------------------------------
145 ----------------------------------------------------
145
146
146 In general, the Admin settings should be self-explanatory and will not be
147 In general, the Admin settings should be self-explanatory and will not be
147 described in more detail in this documentation. However, there are a few
148 described in more detail in this documentation. However, there are a few
148 features that merit further explanation.
149 features that merit further explanation.
149
150
150 Repository extra fields
151 Repository extra fields
151 ~~~~~~~~~~~~~~~~~~~~~~~
152 ~~~~~~~~~~~~~~~~~~~~~~~
152
153
153 In the `Visual` tab, there is an option `Use repository extra
154 In the `Visual` tab, there is an option `Use repository extra
154 fields`, which allows to set custom fields for each repository in the system.
155 fields`, which allows to set custom fields for each repository in the system.
155 Each new field consists of 3 attributes: ``field key``, ``field label``,
156 Each new field consists of 3 attributes: ``field key``, ``field label``,
156 ``field description``.
157 ``field description``.
157
158
158 Example usage of such fields would be to define company-specific information
159 Example usage of such fields would be to define company-specific information
159 into repositories, e.g., defining a ``repo_manager`` key that would give info
160 into repositories, e.g., defining a ``repo_manager`` key that would give info
160 about a manager of each repository. There's no limit for adding custom fields.
161 about a manager of each repository. There's no limit for adding custom fields.
161 Newly created fields are accessible via the API.
162 Newly created fields are accessible via the API.
162
163
163 Meta-Tagging
164 Meta-Tagging
164 ~~~~~~~~~~~~
165 ~~~~~~~~~~~~
165
166
166 In the `Visual` tab, option `Stylify recognised meta tags` will cause Kallithea
167 In the `Visual` tab, option `Stylify recognised meta tags` will cause Kallithea
167 to turn certain meta-tags, detected in repository and repository group
168 to turn certain meta-tags, detected in repository and repository group
168 descriptions, into colored tags. Currently recognised tags are::
169 descriptions, into colored tags. Currently recognised tags are::
169
170
170 [featured]
171 [featured]
171 [stale]
172 [stale]
172 [dead]
173 [dead]
173 [lang => lang]
174 [lang => lang]
174 [license => License]
175 [license => License]
175 [requires => Repo]
176 [requires => Repo]
176 [recommends => Repo]
177 [recommends => Repo]
177 [see => URI]
178 [see => URI]
@@ -1,459 +1,459 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
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 """
15 """
16 kallithea.lib.base
16 kallithea.lib.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
21 controllers
21 controllers
22
22
23 This file was forked by the Kallithea project in July 2014.
23 This file was forked by the Kallithea project in July 2014.
24 Original author and date, and relevant copyright and licensing information is below:
24 Original author and date, and relevant copyright and licensing information is below:
25 :created_on: Oct 06, 2010
25 :created_on: Oct 06, 2010
26 :author: marcink
26 :author: marcink
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
28 :license: GPLv3, see LICENSE.md for more details.
28 :license: GPLv3, see LICENSE.md for more details.
29 """
29 """
30
30
31 import logging
31 import logging
32 import time
32 import time
33 import traceback
33 import traceback
34
34
35 import webob.exc
35 import webob.exc
36 import paste.httpexceptions
36 import paste.httpexceptions
37 import paste.auth.basic
37 import paste.auth.basic
38 import paste.httpheaders
38 import paste.httpheaders
39
39
40 from pylons import config, tmpl_context as c, request, session, url
40 from pylons import config, tmpl_context as c, request, session, url
41 from pylons.controllers import WSGIController
41 from pylons.controllers import WSGIController
42 from pylons.controllers.util import redirect
42 from pylons.controllers.util import redirect
43 from pylons.templating import render_mako as render # don't remove this import
43 from pylons.templating import render_mako as render # don't remove this import
44 from pylons.i18n.translation import _
44 from pylons.i18n.translation import _
45
45
46 from kallithea import __version__, BACKENDS
46 from kallithea import __version__, BACKENDS
47
47
48 from kallithea.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
48 from kallithea.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
49 safe_str, safe_int
49 safe_str, safe_int
50 from kallithea.lib import auth_modules
50 from kallithea.lib import auth_modules
51 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware, CookieStoreWrapper
51 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware, CookieStoreWrapper
52 from kallithea.lib.utils import get_repo_slug
52 from kallithea.lib.utils import get_repo_slug
53 from kallithea.lib.exceptions import UserCreationError
53 from kallithea.lib.exceptions import UserCreationError
54 from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
54 from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
55 from kallithea.model import meta
55 from kallithea.model import meta
56
56
57 from kallithea.model.db import Repository, Ui, User, Setting
57 from kallithea.model.db import Repository, Ui, User, Setting
58 from kallithea.model.notification import NotificationModel
58 from kallithea.model.notification import NotificationModel
59 from kallithea.model.scm import ScmModel
59 from kallithea.model.scm import ScmModel
60 from kallithea.model.pull_request import PullRequestModel
60 from kallithea.model.pull_request import PullRequestModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def _filter_proxy(ip):
65 def _filter_proxy(ip):
66 """
66 """
67 HEADERS can have multiple ips inside the left-most being the original
67 HEADERS can have multiple ips inside the left-most being the original
68 client, and each successive proxy that passed the request adding the IP
68 client, and each successive proxy that passed the request adding the IP
69 address where it received the request from.
69 address where it received the request from.
70
70
71 :param ip:
71 :param ip:
72 """
72 """
73 if ',' in ip:
73 if ',' in ip:
74 _ips = ip.split(',')
74 _ips = ip.split(',')
75 _first_ip = _ips[0].strip()
75 _first_ip = _ips[0].strip()
76 log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
76 log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
77 return _first_ip
77 return _first_ip
78 return ip
78 return ip
79
79
80
80
81 def _get_ip_addr(environ):
81 def _get_ip_addr(environ):
82 proxy_key = 'HTTP_X_REAL_IP'
82 proxy_key = 'HTTP_X_REAL_IP'
83 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
83 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
84 def_key = 'REMOTE_ADDR'
84 def_key = 'REMOTE_ADDR'
85
85
86 ip = environ.get(proxy_key)
86 ip = environ.get(proxy_key)
87 if ip:
87 if ip:
88 return _filter_proxy(ip)
88 return _filter_proxy(ip)
89
89
90 ip = environ.get(proxy_key2)
90 ip = environ.get(proxy_key2)
91 if ip:
91 if ip:
92 return _filter_proxy(ip)
92 return _filter_proxy(ip)
93
93
94 ip = environ.get(def_key, '0.0.0.0')
94 ip = environ.get(def_key, '0.0.0.0')
95 return _filter_proxy(ip)
95 return _filter_proxy(ip)
96
96
97
97
98 def _get_access_path(environ):
98 def _get_access_path(environ):
99 path = environ.get('PATH_INFO')
99 path = environ.get('PATH_INFO')
100 org_req = environ.get('pylons.original_request')
100 org_req = environ.get('pylons.original_request')
101 if org_req:
101 if org_req:
102 path = org_req.environ.get('PATH_INFO')
102 path = org_req.environ.get('PATH_INFO')
103 return path
103 return path
104
104
105
105
106 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
106 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
107
107
108 def __init__(self, realm, authfunc, auth_http_code=None):
108 def __init__(self, realm, authfunc, auth_http_code=None):
109 self.realm = realm
109 self.realm = realm
110 self.authfunc = authfunc
110 self.authfunc = authfunc
111 self._rc_auth_http_code = auth_http_code
111 self._rc_auth_http_code = auth_http_code
112
112
113 def build_authentication(self):
113 def build_authentication(self):
114 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
114 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
115 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
115 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
116 # return 403 if alternative http return code is specified in
116 # return 403 if alternative http return code is specified in
117 # Kallithea config
117 # Kallithea config
118 return paste.httpexceptions.HTTPForbidden(headers=head)
118 return paste.httpexceptions.HTTPForbidden(headers=head)
119 return paste.httpexceptions.HTTPUnauthorized(headers=head)
119 return paste.httpexceptions.HTTPUnauthorized(headers=head)
120
120
121 def authenticate(self, environ):
121 def authenticate(self, environ):
122 authorization = paste.httpheaders.AUTHORIZATION(environ)
122 authorization = paste.httpheaders.AUTHORIZATION(environ)
123 if not authorization:
123 if not authorization:
124 return self.build_authentication()
124 return self.build_authentication()
125 (authmeth, auth) = authorization.split(' ', 1)
125 (authmeth, auth) = authorization.split(' ', 1)
126 if 'basic' != authmeth.lower():
126 if 'basic' != authmeth.lower():
127 return self.build_authentication()
127 return self.build_authentication()
128 auth = auth.strip().decode('base64')
128 auth = auth.strip().decode('base64')
129 _parts = auth.split(':', 1)
129 _parts = auth.split(':', 1)
130 if len(_parts) == 2:
130 if len(_parts) == 2:
131 username, password = _parts
131 username, password = _parts
132 if self.authfunc(username, password, environ):
132 if self.authfunc(username, password, environ):
133 return username
133 return username
134 return self.build_authentication()
134 return self.build_authentication()
135
135
136 __call__ = authenticate
136 __call__ = authenticate
137
137
138
138
139 class BaseVCSController(object):
139 class BaseVCSController(object):
140
140
141 def __init__(self, application, config):
141 def __init__(self, application, config):
142 self.application = application
142 self.application = application
143 self.config = config
143 self.config = config
144 # base path of repo locations
144 # base path of repo locations
145 self.basepath = self.config['base_path']
145 self.basepath = self.config['base_path']
146 #authenticate this VCS request using authfunc
146 #authenticate this VCS request using authfunc
147 self.authenticate = BasicAuth('', auth_modules.authenticate,
147 self.authenticate = BasicAuth('', auth_modules.authenticate,
148 config.get('auth_ret_code'))
148 config.get('auth_ret_code'))
149 self.ip_addr = '0.0.0.0'
149 self.ip_addr = '0.0.0.0'
150
150
151 def _handle_request(self, environ, start_response):
151 def _handle_request(self, environ, start_response):
152 raise NotImplementedError()
152 raise NotImplementedError()
153
153
154 def _get_by_id(self, repo_name):
154 def _get_by_id(self, repo_name):
155 """
155 """
156 Gets a special pattern _<ID> from clone url and tries to replace it
156 Gets a special pattern _<ID> from clone url and tries to replace it
157 with a repository_name for support of _<ID> non changeable urls
157 with a repository_name for support of _<ID> permanent URLs
158
158
159 :param repo_name:
159 :param repo_name:
160 """
160 """
161
161
162 data = repo_name.split('/')
162 data = repo_name.split('/')
163 if len(data) >= 2:
163 if len(data) >= 2:
164 from kallithea.lib.utils import get_repo_by_id
164 from kallithea.lib.utils import get_repo_by_id
165 by_id_match = get_repo_by_id(repo_name)
165 by_id_match = get_repo_by_id(repo_name)
166 if by_id_match:
166 if by_id_match:
167 data[1] = by_id_match
167 data[1] = by_id_match
168
168
169 return '/'.join(data)
169 return '/'.join(data)
170
170
171 def _invalidate_cache(self, repo_name):
171 def _invalidate_cache(self, repo_name):
172 """
172 """
173 Sets cache for this repository for invalidation on next access
173 Sets cache for this repository for invalidation on next access
174
174
175 :param repo_name: full repo name, also a cache key
175 :param repo_name: full repo name, also a cache key
176 """
176 """
177 ScmModel().mark_for_invalidation(repo_name)
177 ScmModel().mark_for_invalidation(repo_name)
178
178
179 def _check_permission(self, action, user, repo_name, ip_addr=None):
179 def _check_permission(self, action, user, repo_name, ip_addr=None):
180 """
180 """
181 Checks permissions using action (push/pull) user and repository
181 Checks permissions using action (push/pull) user and repository
182 name
182 name
183
183
184 :param action: push or pull action
184 :param action: push or pull action
185 :param user: user instance
185 :param user: user instance
186 :param repo_name: repository name
186 :param repo_name: repository name
187 """
187 """
188 # check IP
188 # check IP
189 inherit = user.inherit_default_permissions
189 inherit = user.inherit_default_permissions
190 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
190 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
191 inherit_from_default=inherit)
191 inherit_from_default=inherit)
192 if ip_allowed:
192 if ip_allowed:
193 log.info('Access for IP:%s allowed' % (ip_addr,))
193 log.info('Access for IP:%s allowed' % (ip_addr,))
194 else:
194 else:
195 return False
195 return False
196
196
197 if action == 'push':
197 if action == 'push':
198 if not HasPermissionAnyMiddleware('repository.write',
198 if not HasPermissionAnyMiddleware('repository.write',
199 'repository.admin')(user,
199 'repository.admin')(user,
200 repo_name):
200 repo_name):
201 return False
201 return False
202
202
203 else:
203 else:
204 #any other action need at least read permission
204 #any other action need at least read permission
205 if not HasPermissionAnyMiddleware('repository.read',
205 if not HasPermissionAnyMiddleware('repository.read',
206 'repository.write',
206 'repository.write',
207 'repository.admin')(user,
207 'repository.admin')(user,
208 repo_name):
208 repo_name):
209 return False
209 return False
210
210
211 return True
211 return True
212
212
213 def _get_ip_addr(self, environ):
213 def _get_ip_addr(self, environ):
214 return _get_ip_addr(environ)
214 return _get_ip_addr(environ)
215
215
216 def _check_ssl(self, environ):
216 def _check_ssl(self, environ):
217 """
217 """
218 Checks the SSL check flag and returns False if SSL is not present
218 Checks the SSL check flag and returns False if SSL is not present
219 and required True otherwise
219 and required True otherwise
220 """
220 """
221 #check if we have SSL required ! if not it's a bad request !
221 #check if we have SSL required ! if not it's a bad request !
222 if str2bool(Ui.get_by_key('push_ssl').ui_value):
222 if str2bool(Ui.get_by_key('push_ssl').ui_value):
223 org_proto = environ.get('wsgi._org_proto', environ['wsgi.url_scheme'])
223 org_proto = environ.get('wsgi._org_proto', environ['wsgi.url_scheme'])
224 if org_proto != 'https':
224 if org_proto != 'https':
225 log.debug('proto is %s and SSL is required BAD REQUEST !'
225 log.debug('proto is %s and SSL is required BAD REQUEST !'
226 % org_proto)
226 % org_proto)
227 return False
227 return False
228 return True
228 return True
229
229
230 def _check_locking_state(self, environ, action, repo, user_id):
230 def _check_locking_state(self, environ, action, repo, user_id):
231 """
231 """
232 Checks locking on this repository, if locking is enabled and lock is
232 Checks locking on this repository, if locking is enabled and lock is
233 present returns a tuple of make_lock, locked, locked_by.
233 present returns a tuple of make_lock, locked, locked_by.
234 make_lock can have 3 states None (do nothing) True, make lock
234 make_lock can have 3 states None (do nothing) True, make lock
235 False release lock, This value is later propagated to hooks, which
235 False release lock, This value is later propagated to hooks, which
236 do the locking. Think about this as signals passed to hooks what to do.
236 do the locking. Think about this as signals passed to hooks what to do.
237
237
238 """
238 """
239 locked = False # defines that locked error should be thrown to user
239 locked = False # defines that locked error should be thrown to user
240 make_lock = None
240 make_lock = None
241 repo = Repository.get_by_repo_name(repo)
241 repo = Repository.get_by_repo_name(repo)
242 user = User.get(user_id)
242 user = User.get(user_id)
243
243
244 # this is kind of hacky, but due to how mercurial handles client-server
244 # this is kind of hacky, but due to how mercurial handles client-server
245 # server see all operation on changeset; bookmarks, phases and
245 # server see all operation on changeset; bookmarks, phases and
246 # obsolescence marker in different transaction, we don't want to check
246 # obsolescence marker in different transaction, we don't want to check
247 # locking on those
247 # locking on those
248 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
248 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
249 locked_by = repo.locked
249 locked_by = repo.locked
250 if repo and repo.enable_locking and not obsolete_call:
250 if repo and repo.enable_locking and not obsolete_call:
251 if action == 'push':
251 if action == 'push':
252 #check if it's already locked !, if it is compare users
252 #check if it's already locked !, if it is compare users
253 user_id, _date = repo.locked
253 user_id, _date = repo.locked
254 if user.user_id == user_id:
254 if user.user_id == user_id:
255 log.debug('Got push from user %s, now unlocking' % (user))
255 log.debug('Got push from user %s, now unlocking' % (user))
256 # unlock if we have push from user who locked
256 # unlock if we have push from user who locked
257 make_lock = False
257 make_lock = False
258 else:
258 else:
259 # we're not the same user who locked, ban with 423 !
259 # we're not the same user who locked, ban with 423 !
260 locked = True
260 locked = True
261 if action == 'pull':
261 if action == 'pull':
262 if repo.locked[0] and repo.locked[1]:
262 if repo.locked[0] and repo.locked[1]:
263 locked = True
263 locked = True
264 else:
264 else:
265 log.debug('Setting lock on repo %s by %s' % (repo, user))
265 log.debug('Setting lock on repo %s by %s' % (repo, user))
266 make_lock = True
266 make_lock = True
267
267
268 else:
268 else:
269 log.debug('Repository %s do not have locking enabled' % (repo))
269 log.debug('Repository %s do not have locking enabled' % (repo))
270 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
270 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
271 % (make_lock, locked, locked_by))
271 % (make_lock, locked, locked_by))
272 return make_lock, locked, locked_by
272 return make_lock, locked, locked_by
273
273
274 def __call__(self, environ, start_response):
274 def __call__(self, environ, start_response):
275 start = time.time()
275 start = time.time()
276 try:
276 try:
277 return self._handle_request(environ, start_response)
277 return self._handle_request(environ, start_response)
278 finally:
278 finally:
279 log = logging.getLogger('kallithea.' + self.__class__.__name__)
279 log = logging.getLogger('kallithea.' + self.__class__.__name__)
280 log.debug('Request time: %.3fs' % (time.time() - start))
280 log.debug('Request time: %.3fs' % (time.time() - start))
281 meta.Session.remove()
281 meta.Session.remove()
282
282
283
283
284 class BaseController(WSGIController):
284 class BaseController(WSGIController):
285
285
286 def __before__(self):
286 def __before__(self):
287 """
287 """
288 __before__ is called before controller methods and after __call__
288 __before__ is called before controller methods and after __call__
289 """
289 """
290 c.kallithea_version = __version__
290 c.kallithea_version = __version__
291 rc_config = Setting.get_app_settings()
291 rc_config = Setting.get_app_settings()
292
292
293 # Visual options
293 # Visual options
294 c.visual = AttributeDict({})
294 c.visual = AttributeDict({})
295
295
296 ## DB stored
296 ## DB stored
297 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
297 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
298 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
298 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
299 c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags'))
299 c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags'))
300 c.visual.dashboard_items = safe_int(rc_config.get('dashboard_items', 100))
300 c.visual.dashboard_items = safe_int(rc_config.get('dashboard_items', 100))
301 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
301 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
302 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
302 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
303 c.visual.show_version = str2bool(rc_config.get('show_version'))
303 c.visual.show_version = str2bool(rc_config.get('show_version'))
304 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
304 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
305 c.visual.gravatar_url = rc_config.get('gravatar_url')
305 c.visual.gravatar_url = rc_config.get('gravatar_url')
306
306
307 c.ga_code = rc_config.get('ga_code')
307 c.ga_code = rc_config.get('ga_code')
308 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
308 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
309 if c.ga_code and '<' not in c.ga_code:
309 if c.ga_code and '<' not in c.ga_code:
310 c.ga_code = '''<script type="text/javascript">
310 c.ga_code = '''<script type="text/javascript">
311 var _gaq = _gaq || [];
311 var _gaq = _gaq || [];
312 _gaq.push(['_setAccount', '%s']);
312 _gaq.push(['_setAccount', '%s']);
313 _gaq.push(['_trackPageview']);
313 _gaq.push(['_trackPageview']);
314
314
315 (function() {
315 (function() {
316 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
316 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
317 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
317 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
318 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
318 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
319 })();
319 })();
320 </script>''' % c.ga_code
320 </script>''' % c.ga_code
321 c.site_name = rc_config.get('title')
321 c.site_name = rc_config.get('title')
322 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl')
322 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl')
323
323
324 ## INI stored
324 ## INI stored
325 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
325 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
326 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
326 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
327
327
328 c.instance_id = config.get('instance_id')
328 c.instance_id = config.get('instance_id')
329 c.issues_url = config.get('bugtracker', url('issues_url'))
329 c.issues_url = config.get('bugtracker', url('issues_url'))
330 # END CONFIG VARS
330 # END CONFIG VARS
331
331
332 c.repo_name = get_repo_slug(request) # can be empty
332 c.repo_name = get_repo_slug(request) # can be empty
333 c.backends = BACKENDS.keys()
333 c.backends = BACKENDS.keys()
334 c.unread_notifications = NotificationModel()\
334 c.unread_notifications = NotificationModel()\
335 .get_unread_cnt_for_user(c.authuser.user_id)
335 .get_unread_cnt_for_user(c.authuser.user_id)
336
336
337 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
337 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
338
338
339 c.my_pr_count = PullRequestModel().get_pullrequest_cnt_for_user(c.authuser.user_id)
339 c.my_pr_count = PullRequestModel().get_pullrequest_cnt_for_user(c.authuser.user_id)
340
340
341 self.sa = meta.Session
341 self.sa = meta.Session
342 self.scm_model = ScmModel(self.sa)
342 self.scm_model = ScmModel(self.sa)
343
343
344 def __call__(self, environ, start_response):
344 def __call__(self, environ, start_response):
345 """Invoke the Controller"""
345 """Invoke the Controller"""
346 # WSGIController.__call__ dispatches to the Controller method
346 # WSGIController.__call__ dispatches to the Controller method
347 # the request is routed to. This routing information is
347 # the request is routed to. This routing information is
348 # available in environ['pylons.routes_dict']
348 # available in environ['pylons.routes_dict']
349 try:
349 try:
350 self.ip_addr = _get_ip_addr(environ)
350 self.ip_addr = _get_ip_addr(environ)
351 # make sure that we update permissions each time we call controller
351 # make sure that we update permissions each time we call controller
352 api_key = request.GET.get('api_key')
352 api_key = request.GET.get('api_key')
353
353
354 if api_key:
354 if api_key:
355 # when using API_KEY we are sure user exists.
355 # when using API_KEY we are sure user exists.
356 auth_user = AuthUser(api_key=api_key, ip_addr=self.ip_addr)
356 auth_user = AuthUser(api_key=api_key, ip_addr=self.ip_addr)
357 authenticated = False
357 authenticated = False
358 else:
358 else:
359 cookie_store = CookieStoreWrapper(session.get('authuser'))
359 cookie_store = CookieStoreWrapper(session.get('authuser'))
360 try:
360 try:
361 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
361 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
362 ip_addr=self.ip_addr)
362 ip_addr=self.ip_addr)
363 except UserCreationError, e:
363 except UserCreationError, e:
364 from kallithea.lib import helpers as h
364 from kallithea.lib import helpers as h
365 h.flash(e, 'error')
365 h.flash(e, 'error')
366 # container auth or other auth functions that create users on
366 # container auth or other auth functions that create users on
367 # the fly can throw this exception signaling that there's issue
367 # the fly can throw this exception signaling that there's issue
368 # with user creation, explanation should be provided in
368 # with user creation, explanation should be provided in
369 # Exception itself
369 # Exception itself
370 auth_user = AuthUser(ip_addr=self.ip_addr)
370 auth_user = AuthUser(ip_addr=self.ip_addr)
371
371
372 authenticated = cookie_store.get('is_authenticated')
372 authenticated = cookie_store.get('is_authenticated')
373
373
374 if not auth_user.is_authenticated and auth_user.user_id is not None:
374 if not auth_user.is_authenticated and auth_user.user_id is not None:
375 # user is not authenticated and not empty
375 # user is not authenticated and not empty
376 auth_user.set_authenticated(authenticated)
376 auth_user.set_authenticated(authenticated)
377 request.user = auth_user
377 request.user = auth_user
378 #set globals for auth user
378 #set globals for auth user
379 self.authuser = c.authuser = auth_user
379 self.authuser = c.authuser = auth_user
380 log.info('IP: %s User: %s accessed %s' % (
380 log.info('IP: %s User: %s accessed %s' % (
381 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
381 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
382 )
382 )
383 return WSGIController.__call__(self, environ, start_response)
383 return WSGIController.__call__(self, environ, start_response)
384 finally:
384 finally:
385 meta.Session.remove()
385 meta.Session.remove()
386
386
387
387
388 class BaseRepoController(BaseController):
388 class BaseRepoController(BaseController):
389 """
389 """
390 Base class for controllers responsible for loading all needed data for
390 Base class for controllers responsible for loading all needed data for
391 repository loaded items are
391 repository loaded items are
392
392
393 c.db_repo_scm_instance: instance of scm repository
393 c.db_repo_scm_instance: instance of scm repository
394 c.db_repo: instance of db
394 c.db_repo: instance of db
395 c.repository_followers: number of followers
395 c.repository_followers: number of followers
396 c.repository_forks: number of forks
396 c.repository_forks: number of forks
397 c.repository_following: weather the current user is following the current repo
397 c.repository_following: weather the current user is following the current repo
398 """
398 """
399
399
400 def __before__(self):
400 def __before__(self):
401 super(BaseRepoController, self).__before__()
401 super(BaseRepoController, self).__before__()
402 if c.repo_name: # extracted from routes
402 if c.repo_name: # extracted from routes
403 _dbr = Repository.get_by_repo_name(c.repo_name)
403 _dbr = Repository.get_by_repo_name(c.repo_name)
404 if not _dbr:
404 if not _dbr:
405 return
405 return
406
406
407 log.debug('Found repository in database %s with state `%s`'
407 log.debug('Found repository in database %s with state `%s`'
408 % (safe_unicode(_dbr), safe_unicode(_dbr.repo_state)))
408 % (safe_unicode(_dbr), safe_unicode(_dbr.repo_state)))
409 route = getattr(request.environ.get('routes.route'), 'name', '')
409 route = getattr(request.environ.get('routes.route'), 'name', '')
410
410
411 # allow to delete repos that are somehow damages in filesystem
411 # allow to delete repos that are somehow damages in filesystem
412 if route in ['delete_repo']:
412 if route in ['delete_repo']:
413 return
413 return
414
414
415 if _dbr.repo_state in [Repository.STATE_PENDING]:
415 if _dbr.repo_state in [Repository.STATE_PENDING]:
416 if route in ['repo_creating_home']:
416 if route in ['repo_creating_home']:
417 return
417 return
418 check_url = url('repo_creating_home', repo_name=c.repo_name)
418 check_url = url('repo_creating_home', repo_name=c.repo_name)
419 return redirect(check_url)
419 return redirect(check_url)
420
420
421 dbr = c.db_repo = _dbr
421 dbr = c.db_repo = _dbr
422 c.db_repo_scm_instance = c.db_repo.scm_instance
422 c.db_repo_scm_instance = c.db_repo.scm_instance
423 if c.db_repo_scm_instance is None:
423 if c.db_repo_scm_instance is None:
424 log.error('%s this repository is present in database but it '
424 log.error('%s this repository is present in database but it '
425 'cannot be created as an scm instance', c.repo_name)
425 'cannot be created as an scm instance', c.repo_name)
426 from kallithea.lib import helpers as h
426 from kallithea.lib import helpers as h
427 h.flash(h.literal(_('Repository not found in the filesystem')),
427 h.flash(h.literal(_('Repository not found in the filesystem')),
428 category='error')
428 category='error')
429 raise paste.httpexceptions.HTTPNotFound()
429 raise paste.httpexceptions.HTTPNotFound()
430
430
431 # some globals counter for menu
431 # some globals counter for menu
432 c.repository_followers = self.scm_model.get_followers(dbr)
432 c.repository_followers = self.scm_model.get_followers(dbr)
433 c.repository_forks = self.scm_model.get_forks(dbr)
433 c.repository_forks = self.scm_model.get_forks(dbr)
434 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
434 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
435 c.repository_following = self.scm_model.is_following_repo(
435 c.repository_following = self.scm_model.is_following_repo(
436 c.repo_name, self.authuser.user_id)
436 c.repo_name, self.authuser.user_id)
437
437
438 @staticmethod
438 @staticmethod
439 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
439 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
440 """
440 """
441 Safe way to get changeset. If error occurs show error.
441 Safe way to get changeset. If error occurs show error.
442 """
442 """
443 from kallithea.lib import helpers as h
443 from kallithea.lib import helpers as h
444 try:
444 try:
445 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
445 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
446 except EmptyRepositoryError as e:
446 except EmptyRepositoryError as e:
447 if returnempty:
447 if returnempty:
448 return repo.scm_instance.EMPTY_CHANGESET
448 return repo.scm_instance.EMPTY_CHANGESET
449 h.flash(h.literal(_('There are no changesets yet')),
449 h.flash(h.literal(_('There are no changesets yet')),
450 category='error')
450 category='error')
451 raise webob.exc.HTTPNotFound()
451 raise webob.exc.HTTPNotFound()
452 except ChangesetDoesNotExistError as e:
452 except ChangesetDoesNotExistError as e:
453 h.flash(h.literal(_('Changeset not found')),
453 h.flash(h.literal(_('Changeset not found')),
454 category='error')
454 category='error')
455 raise webob.exc.HTTPNotFound()
455 raise webob.exc.HTTPNotFound()
456 except RepositoryError as e:
456 except RepositoryError as e:
457 log.error(traceback.format_exc())
457 log.error(traceback.format_exc())
458 h.flash(safe_str(e), category='error')
458 h.flash(safe_str(e), category='error')
459 raise webob.exc.HTTPBadRequest()
459 raise webob.exc.HTTPBadRequest()
@@ -1,164 +1,164 b''
1 ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='put')}
1 ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='put')}
2 <div class="form">
2 <div class="form">
3 <!-- fields -->
3 <!-- fields -->
4 <div class="fields">
4 <div class="fields">
5 <div class="field">
5 <div class="field">
6 <div class="label">
6 <div class="label">
7 <label for="repo_name">${_('Name')}:</label>
7 <label for="repo_name">${_('Name')}:</label>
8 </div>
8 </div>
9 <div class="input">
9 <div class="input">
10 ${h.text('repo_name',class_="medium")}
10 ${h.text('repo_name',class_="medium")}
11 <span class="help-block">${_('Non-changeable id')}: `_${c.repo_info.repo_id}` <span><a id="show_more_clone_id" href="#">${_('What is that?')}</a></span></span>
11 <span class="help-block">${_('Permanent Repository ID')}: `_${c.repo_info.repo_id}` <span><a id="show_more_clone_id" href="#">${_('What is that?')}</a></span></span>
12 <span id="clone_id" class="help-block" style="display: none">
12 <span id="clone_id" class="help-block" style="display: none">
13 ${_('URL by id')}: `${c.repo_info.clone_url(with_id=True)}` </br>
13 ${_('URL by id')}: `${c.repo_info.clone_url(with_id=True)}` </br>
14 ${_('''In case this repository is renamed or moved into another group the repository URL changes.
14 ${_('''In case this repository is renamed or moved into another group the repository URL changes.
15 Using the above URL guarantees that this repository will always be accessible under such URL.
15 Using the above permanent URL guarantees that this repository always will be accessible on that URL.
16 Useful for CI systems, or any other cases that you need to hardcode the URL into 3rd party service.''')}</span>
16 This is useful for CI systems, or any other cases that you need to hardcode the URL into a 3rd party service.''')}</span>
17 </div>
17 </div>
18 </div>
18 </div>
19 <div class="field">
19 <div class="field">
20 <div class="label">
20 <div class="label">
21 <label for="clone_uri">${_('Clone URL')}:</label>
21 <label for="clone_uri">${_('Clone URL')}:</label>
22 </div>
22 </div>
23 <div class="input">
23 <div class="input">
24 %if c.repo_info.clone_uri:
24 %if c.repo_info.clone_uri:
25 <div id="clone_uri_hidden" style="font-size: 14px">
25 <div id="clone_uri_hidden" style="font-size: 14px">
26 <span id="clone_uri_hidden_value">${c.repo_info.clone_uri_hidden}</span>
26 <span id="clone_uri_hidden_value">${c.repo_info.clone_uri_hidden}</span>
27 <span style="cursor: pointer; padding: 0px 0px 5px 0px" id="edit_clone_uri"><i class="icon-edit"></i>${_('edit')}</span>
27 <span style="cursor: pointer; padding: 0px 0px 5px 0px" id="edit_clone_uri"><i class="icon-edit"></i>${_('edit')}</span>
28 </div>
28 </div>
29 <div id="alter_clone_uri" style="display: none">
29 <div id="alter_clone_uri" style="display: none">
30 ${h.text('clone_uri',class_="medium", placeholder=_('new value'))}
30 ${h.text('clone_uri',class_="medium", placeholder=_('new value'))}
31 </div>
31 </div>
32 %else:
32 %else:
33 ## not set yet, display form to set it
33 ## not set yet, display form to set it
34 ${h.text('clone_uri',class_="medium")}
34 ${h.text('clone_uri',class_="medium")}
35 ${h.hidden('clone_uri_change', 'NEW')}
35 ${h.hidden('clone_uri_change', 'NEW')}
36 %endif
36 %endif
37 <span id="alter_clone_uri_help_block" class="help-block">${_('URL used for doing remote pulls.')}</span>
37 <span id="alter_clone_uri_help_block" class="help-block">${_('URL used for doing remote pulls.')}</span>
38 </div>
38 </div>
39 </div>
39 </div>
40 <div class="field">
40 <div class="field">
41 <div class="label">
41 <div class="label">
42 <label for="repo_group">${_('Repository group')}:</label>
42 <label for="repo_group">${_('Repository group')}:</label>
43 </div>
43 </div>
44 <div class="input">
44 <div class="input">
45 ${h.select('repo_group','',c.repo_groups,class_="medium")}
45 ${h.select('repo_group','',c.repo_groups,class_="medium")}
46 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
46 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
47 </div>
47 </div>
48 </div>
48 </div>
49 <div class="field">
49 <div class="field">
50 <div class="label">
50 <div class="label">
51 <label for="repo_landing_rev">${_('Landing revision')}:</label>
51 <label for="repo_landing_rev">${_('Landing revision')}:</label>
52 </div>
52 </div>
53 <div class="input">
53 <div class="input">
54 ${h.select('repo_landing_rev','',c.landing_revs,class_="medium")}
54 ${h.select('repo_landing_rev','',c.landing_revs,class_="medium")}
55 <span class="help-block">${_('Default revision for files page, downloads, whoosh and readme')}</span>
55 <span class="help-block">${_('Default revision for files page, downloads, whoosh and readme')}</span>
56 </div>
56 </div>
57 </div>
57 </div>
58 <div class="field">
58 <div class="field">
59 <div class="label">
59 <div class="label">
60 <label for="user">${_('Owner')}:</label>
60 <label for="user">${_('Owner')}:</label>
61 </div>
61 </div>
62 <div class="input input-medium ac">
62 <div class="input input-medium ac">
63 <div class="perm_ac">
63 <div class="perm_ac">
64 ${h.text('user',class_='yui-ac-input')}
64 ${h.text('user',class_='yui-ac-input')}
65 <span class="help-block">${_('Change owner of this repository.')}</span>
65 <span class="help-block">${_('Change owner of this repository.')}</span>
66 <div id="owner_container"></div>
66 <div id="owner_container"></div>
67 </div>
67 </div>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="field">
70 <div class="field">
71 <div class="label label-textarea">
71 <div class="label label-textarea">
72 <label for="repo_description">${_('Description')}:</label>
72 <label for="repo_description">${_('Description')}:</label>
73 </div>
73 </div>
74 <div class="textarea text-area editor">
74 <div class="textarea text-area editor">
75 ${h.textarea('repo_description', style="height:165px")}
75 ${h.textarea('repo_description', style="height:165px")}
76 <span class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</span>
76 <span class="help-block">${_('Keep it short and to the point. Use a README file for longer descriptions.')}</span>
77 </div>
77 </div>
78 </div>
78 </div>
79
79
80 <div class="field">
80 <div class="field">
81 <div class="label label-checkbox">
81 <div class="label label-checkbox">
82 <label for="repo_private">${_('Private repository')}:</label>
82 <label for="repo_private">${_('Private repository')}:</label>
83 </div>
83 </div>
84 <div class="checkboxes">
84 <div class="checkboxes">
85 ${h.checkbox('repo_private',value="True")}
85 ${h.checkbox('repo_private',value="True")}
86 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
86 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
87 </div>
87 </div>
88 </div>
88 </div>
89 <div class="field">
89 <div class="field">
90 <div class="label label-checkbox">
90 <div class="label label-checkbox">
91 <label for="repo_enable_statistics">${_('Enable statistics')}:</label>
91 <label for="repo_enable_statistics">${_('Enable statistics')}:</label>
92 </div>
92 </div>
93 <div class="checkboxes">
93 <div class="checkboxes">
94 ${h.checkbox('repo_enable_statistics',value="True")}
94 ${h.checkbox('repo_enable_statistics',value="True")}
95 <span class="help-block">${_('Enable statistics window on summary page.')}</span>
95 <span class="help-block">${_('Enable statistics window on summary page.')}</span>
96 </div>
96 </div>
97 </div>
97 </div>
98 <div class="field">
98 <div class="field">
99 <div class="label label-checkbox">
99 <div class="label label-checkbox">
100 <label for="repo_enable_downloads">${_('Enable downloads')}:</label>
100 <label for="repo_enable_downloads">${_('Enable downloads')}:</label>
101 </div>
101 </div>
102 <div class="checkboxes">
102 <div class="checkboxes">
103 ${h.checkbox('repo_enable_downloads',value="True")}
103 ${h.checkbox('repo_enable_downloads',value="True")}
104 <span class="help-block">${_('Enable download menu on summary page.')}</span>
104 <span class="help-block">${_('Enable download menu on summary page.')}</span>
105 </div>
105 </div>
106 </div>
106 </div>
107 <div class="field">
107 <div class="field">
108 <div class="label label-checkbox">
108 <div class="label label-checkbox">
109 <label for="repo_enable_locking">${_('Enable locking')}:</label>
109 <label for="repo_enable_locking">${_('Enable locking')}:</label>
110 </div>
110 </div>
111 <div class="checkboxes">
111 <div class="checkboxes">
112 ${h.checkbox('repo_enable_locking',value="True")}
112 ${h.checkbox('repo_enable_locking',value="True")}
113 <span class="help-block">${_('Enable lock-by-pulling on repository.')}</span>
113 <span class="help-block">${_('Enable lock-by-pulling on repository.')}</span>
114 </div>
114 </div>
115 </div>
115 </div>
116
116
117 %if c.visual.repository_fields:
117 %if c.visual.repository_fields:
118 ## EXTRA FIELDS
118 ## EXTRA FIELDS
119 %for field in c.repo_fields:
119 %for field in c.repo_fields:
120 <div class="field">
120 <div class="field">
121 <div class="label">
121 <div class="label">
122 <label for="${field.field_key_prefixed}">${field.field_label} (${field.field_key}):</label>
122 <label for="${field.field_key_prefixed}">${field.field_label} (${field.field_key}):</label>
123 </div>
123 </div>
124 <div class="input input-medium">
124 <div class="input input-medium">
125 ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
125 ${h.text(field.field_key_prefixed, field.field_value, class_='medium')}
126 %if field.field_desc:
126 %if field.field_desc:
127 <span class="help-block">${field.field_desc}</span>
127 <span class="help-block">${field.field_desc}</span>
128 %endif
128 %endif
129 </div>
129 </div>
130 </div>
130 </div>
131 %endfor
131 %endfor
132 %endif
132 %endif
133 <div class="buttons">
133 <div class="buttons">
134 ${h.submit('save',_('Save'),class_="btn")}
134 ${h.submit('save',_('Save'),class_="btn")}
135 ${h.reset('reset',_('Reset'),class_="btn")}
135 ${h.reset('reset',_('Reset'),class_="btn")}
136 </div>
136 </div>
137 </div>
137 </div>
138 </div>
138 </div>
139 ${h.end_form()}
139 ${h.end_form()}
140
140
141 <script>
141 <script>
142 $(document).ready(function(){
142 $(document).ready(function(){
143 $('#show_more_clone_id').on('click', function(e){
143 $('#show_more_clone_id').on('click', function(e){
144 $('#clone_id').show();
144 $('#clone_id').show();
145 e.preventDefault();
145 e.preventDefault();
146 })
146 })
147 $('#edit_clone_uri').on('click', function(e){
147 $('#edit_clone_uri').on('click', function(e){
148 $('#alter_clone_uri').show();
148 $('#alter_clone_uri').show();
149 $('#edit_clone_uri').hide();
149 $('#edit_clone_uri').hide();
150 $('#clone_uri_hidden').hide();
150 $('#clone_uri_hidden').hide();
151 ## store hash of old value for change detection
151 ## store hash of old value for change detection
152 var uri_change = '<input id="clone_uri_change" name="clone_uri_change" type="hidden" value="${h.md5(c.repo_info.clone_uri or "").hexdigest()}" />';
152 var uri_change = '<input id="clone_uri_change" name="clone_uri_change" type="hidden" value="${h.md5(c.repo_info.clone_uri or "").hexdigest()}" />';
153 $('#alter_clone_uri_help_block').html($('#alter_clone_uri_help_block').html()+" ("+$('#clone_uri_hidden_value').html()+")")
153 $('#alter_clone_uri_help_block').html($('#alter_clone_uri_help_block').html()+" ("+$('#clone_uri_hidden_value').html()+")")
154 })
154 })
155
155
156 $('#repo_landing_rev').select2({
156 $('#repo_landing_rev').select2({
157 'dropdownAutoWidth': true
157 'dropdownAutoWidth': true
158 });
158 });
159 $('#repo_group').select2({
159 $('#repo_group').select2({
160 'dropdownAutoWidth': true
160 'dropdownAutoWidth': true
161 });
161 });
162
162
163 })
163 })
164 </script>
164 </script>
General Comments 0
You need to be logged in to leave comments. Login now