##// END OF EJS Templates
api: implemente missing get_repo_refs api backend. Fixes #4677
marcink -
r1243:7c9d8d89 default
parent child Browse files
Show More
@@ -0,0 +1,40 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import pytest
23
24 from rhodecode.model.meta import Session
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.user import UserModel
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
28 from rhodecode.api.tests.utils import (
29 build_data, api_call, assert_ok, assert_error, expected_permissions)
30
31
32 @pytest.mark.usefixtures("testuser_api", "app")
33 class TestGetRepo(object):
34 def test_api_get_repo_refs(self, backend, user_util):
35 repo = backend.create_repo()
36 id_, params = build_data(self.apikey, 'get_repo_refs',
37 **{'repoid': repo.repo_name,})
38 response = api_call(self.app, params)
39 expected = repo.scm_instance().refs()
40 assert_ok(id_, expected, given=response.body)
@@ -1,1918 +1,1932 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
32 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
33 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
33 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
34 from rhodecode.lib.utils2 import str2bool, time_to_datetime
34 from rhodecode.lib.utils2 import str2bool, time_to_datetime
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.model.changeset_status import ChangesetStatusModel
36 from rhodecode.model.changeset_status import ChangesetStatusModel
37 from rhodecode.model.comment import ChangesetCommentsModel
37 from rhodecode.model.comment import ChangesetCommentsModel
38 from rhodecode.model.db import (
38 from rhodecode.model.db import (
39 Session, ChangesetStatus, RepositoryField, Repository)
39 Session, ChangesetStatus, RepositoryField, Repository)
40 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.scm import ScmModel, RepoList
41 from rhodecode.model.scm import ScmModel, RepoList
42 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
42 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
43 from rhodecode.model import validation_schema
43 from rhodecode.model import validation_schema
44 from rhodecode.model.validation_schema.schemas import repo_schema
44 from rhodecode.model.validation_schema.schemas import repo_schema
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 @jsonrpc_method()
49 @jsonrpc_method()
50 def get_repo(request, apiuser, repoid, cache=Optional(True)):
50 def get_repo(request, apiuser, repoid, cache=Optional(True)):
51 """
51 """
52 Gets an existing repository by its name or repository_id.
52 Gets an existing repository by its name or repository_id.
53
53
54 The members section so the output returns users groups or users
54 The members section so the output returns users groups or users
55 associated with that repository.
55 associated with that repository.
56
56
57 This command can only be run using an |authtoken| with admin rights,
57 This command can only be run using an |authtoken| with admin rights,
58 or users with at least read rights to the |repo|.
58 or users with at least read rights to the |repo|.
59
59
60 :param apiuser: This is filled automatically from the |authtoken|.
60 :param apiuser: This is filled automatically from the |authtoken|.
61 :type apiuser: AuthUser
61 :type apiuser: AuthUser
62 :param repoid: The repository name or repository id.
62 :param repoid: The repository name or repository id.
63 :type repoid: str or int
63 :type repoid: str or int
64 :param cache: use the cached value for last changeset
64 :param cache: use the cached value for last changeset
65 :type: cache: Optional(bool)
65 :type: cache: Optional(bool)
66
66
67 Example output:
67 Example output:
68
68
69 .. code-block:: bash
69 .. code-block:: bash
70
70
71 {
71 {
72 "error": null,
72 "error": null,
73 "id": <repo_id>,
73 "id": <repo_id>,
74 "result": {
74 "result": {
75 "clone_uri": null,
75 "clone_uri": null,
76 "created_on": "timestamp",
76 "created_on": "timestamp",
77 "description": "repo description",
77 "description": "repo description",
78 "enable_downloads": false,
78 "enable_downloads": false,
79 "enable_locking": false,
79 "enable_locking": false,
80 "enable_statistics": false,
80 "enable_statistics": false,
81 "followers": [
81 "followers": [
82 {
82 {
83 "active": true,
83 "active": true,
84 "admin": false,
84 "admin": false,
85 "api_key": "****************************************",
85 "api_key": "****************************************",
86 "api_keys": [
86 "api_keys": [
87 "****************************************"
87 "****************************************"
88 ],
88 ],
89 "email": "user@example.com",
89 "email": "user@example.com",
90 "emails": [
90 "emails": [
91 "user@example.com"
91 "user@example.com"
92 ],
92 ],
93 "extern_name": "rhodecode",
93 "extern_name": "rhodecode",
94 "extern_type": "rhodecode",
94 "extern_type": "rhodecode",
95 "firstname": "username",
95 "firstname": "username",
96 "ip_addresses": [],
96 "ip_addresses": [],
97 "language": null,
97 "language": null,
98 "last_login": "2015-09-16T17:16:35.854",
98 "last_login": "2015-09-16T17:16:35.854",
99 "lastname": "surname",
99 "lastname": "surname",
100 "user_id": <user_id>,
100 "user_id": <user_id>,
101 "username": "name"
101 "username": "name"
102 }
102 }
103 ],
103 ],
104 "fork_of": "parent-repo",
104 "fork_of": "parent-repo",
105 "landing_rev": [
105 "landing_rev": [
106 "rev",
106 "rev",
107 "tip"
107 "tip"
108 ],
108 ],
109 "last_changeset": {
109 "last_changeset": {
110 "author": "User <user@example.com>",
110 "author": "User <user@example.com>",
111 "branch": "default",
111 "branch": "default",
112 "date": "timestamp",
112 "date": "timestamp",
113 "message": "last commit message",
113 "message": "last commit message",
114 "parents": [
114 "parents": [
115 {
115 {
116 "raw_id": "commit-id"
116 "raw_id": "commit-id"
117 }
117 }
118 ],
118 ],
119 "raw_id": "commit-id",
119 "raw_id": "commit-id",
120 "revision": <revision number>,
120 "revision": <revision number>,
121 "short_id": "short id"
121 "short_id": "short id"
122 },
122 },
123 "lock_reason": null,
123 "lock_reason": null,
124 "locked_by": null,
124 "locked_by": null,
125 "locked_date": null,
125 "locked_date": null,
126 "members": [
126 "members": [
127 {
127 {
128 "name": "super-admin-name",
128 "name": "super-admin-name",
129 "origin": "super-admin",
129 "origin": "super-admin",
130 "permission": "repository.admin",
130 "permission": "repository.admin",
131 "type": "user"
131 "type": "user"
132 },
132 },
133 {
133 {
134 "name": "owner-name",
134 "name": "owner-name",
135 "origin": "owner",
135 "origin": "owner",
136 "permission": "repository.admin",
136 "permission": "repository.admin",
137 "type": "user"
137 "type": "user"
138 },
138 },
139 {
139 {
140 "name": "user-group-name",
140 "name": "user-group-name",
141 "origin": "permission",
141 "origin": "permission",
142 "permission": "repository.write",
142 "permission": "repository.write",
143 "type": "user_group"
143 "type": "user_group"
144 }
144 }
145 ],
145 ],
146 "owner": "owner-name",
146 "owner": "owner-name",
147 "permissions": [
147 "permissions": [
148 {
148 {
149 "name": "super-admin-name",
149 "name": "super-admin-name",
150 "origin": "super-admin",
150 "origin": "super-admin",
151 "permission": "repository.admin",
151 "permission": "repository.admin",
152 "type": "user"
152 "type": "user"
153 },
153 },
154 {
154 {
155 "name": "owner-name",
155 "name": "owner-name",
156 "origin": "owner",
156 "origin": "owner",
157 "permission": "repository.admin",
157 "permission": "repository.admin",
158 "type": "user"
158 "type": "user"
159 },
159 },
160 {
160 {
161 "name": "user-group-name",
161 "name": "user-group-name",
162 "origin": "permission",
162 "origin": "permission",
163 "permission": "repository.write",
163 "permission": "repository.write",
164 "type": "user_group"
164 "type": "user_group"
165 }
165 }
166 ],
166 ],
167 "private": true,
167 "private": true,
168 "repo_id": 676,
168 "repo_id": 676,
169 "repo_name": "user-group/repo-name",
169 "repo_name": "user-group/repo-name",
170 "repo_type": "hg"
170 "repo_type": "hg"
171 }
171 }
172 }
172 }
173 """
173 """
174
174
175 repo = get_repo_or_error(repoid)
175 repo = get_repo_or_error(repoid)
176 cache = Optional.extract(cache)
176 cache = Optional.extract(cache)
177
177
178 include_secrets = False
178 include_secrets = False
179 if has_superadmin_permission(apiuser):
179 if has_superadmin_permission(apiuser):
180 include_secrets = True
180 include_secrets = True
181 else:
181 else:
182 # check if we have at least read permission for this repo !
182 # check if we have at least read permission for this repo !
183 _perms = (
183 _perms = (
184 'repository.admin', 'repository.write', 'repository.read',)
184 'repository.admin', 'repository.write', 'repository.read',)
185 validate_repo_permissions(apiuser, repoid, repo, _perms)
185 validate_repo_permissions(apiuser, repoid, repo, _perms)
186
186
187 permissions = []
187 permissions = []
188 for _user in repo.permissions():
188 for _user in repo.permissions():
189 user_data = {
189 user_data = {
190 'name': _user.username,
190 'name': _user.username,
191 'permission': _user.permission,
191 'permission': _user.permission,
192 'origin': get_origin(_user),
192 'origin': get_origin(_user),
193 'type': "user",
193 'type': "user",
194 }
194 }
195 permissions.append(user_data)
195 permissions.append(user_data)
196
196
197 for _user_group in repo.permission_user_groups():
197 for _user_group in repo.permission_user_groups():
198 user_group_data = {
198 user_group_data = {
199 'name': _user_group.users_group_name,
199 'name': _user_group.users_group_name,
200 'permission': _user_group.permission,
200 'permission': _user_group.permission,
201 'origin': get_origin(_user_group),
201 'origin': get_origin(_user_group),
202 'type': "user_group",
202 'type': "user_group",
203 }
203 }
204 permissions.append(user_group_data)
204 permissions.append(user_group_data)
205
205
206 following_users = [
206 following_users = [
207 user.user.get_api_data(include_secrets=include_secrets)
207 user.user.get_api_data(include_secrets=include_secrets)
208 for user in repo.followers]
208 for user in repo.followers]
209
209
210 if not cache:
210 if not cache:
211 repo.update_commit_cache()
211 repo.update_commit_cache()
212 data = repo.get_api_data(include_secrets=include_secrets)
212 data = repo.get_api_data(include_secrets=include_secrets)
213 data['members'] = permissions # TODO: this should be deprecated soon
213 data['members'] = permissions # TODO: this should be deprecated soon
214 data['permissions'] = permissions
214 data['permissions'] = permissions
215 data['followers'] = following_users
215 data['followers'] = following_users
216 return data
216 return data
217
217
218
218
219 @jsonrpc_method()
219 @jsonrpc_method()
220 def get_repos(request, apiuser):
220 def get_repos(request, apiuser):
221 """
221 """
222 Lists all existing repositories.
222 Lists all existing repositories.
223
223
224 This command can only be run using an |authtoken| with admin rights,
224 This command can only be run using an |authtoken| with admin rights,
225 or users with at least read rights to |repos|.
225 or users with at least read rights to |repos|.
226
226
227 :param apiuser: This is filled automatically from the |authtoken|.
227 :param apiuser: This is filled automatically from the |authtoken|.
228 :type apiuser: AuthUser
228 :type apiuser: AuthUser
229
229
230 Example output:
230 Example output:
231
231
232 .. code-block:: bash
232 .. code-block:: bash
233
233
234 id : <id_given_in_input>
234 id : <id_given_in_input>
235 result: [
235 result: [
236 {
236 {
237 "repo_id" : "<repo_id>",
237 "repo_id" : "<repo_id>",
238 "repo_name" : "<reponame>"
238 "repo_name" : "<reponame>"
239 "repo_type" : "<repo_type>",
239 "repo_type" : "<repo_type>",
240 "clone_uri" : "<clone_uri>",
240 "clone_uri" : "<clone_uri>",
241 "private": : "<bool>",
241 "private": : "<bool>",
242 "created_on" : "<datetimecreated>",
242 "created_on" : "<datetimecreated>",
243 "description" : "<description>",
243 "description" : "<description>",
244 "landing_rev": "<landing_rev>",
244 "landing_rev": "<landing_rev>",
245 "owner": "<repo_owner>",
245 "owner": "<repo_owner>",
246 "fork_of": "<name_of_fork_parent>",
246 "fork_of": "<name_of_fork_parent>",
247 "enable_downloads": "<bool>",
247 "enable_downloads": "<bool>",
248 "enable_locking": "<bool>",
248 "enable_locking": "<bool>",
249 "enable_statistics": "<bool>",
249 "enable_statistics": "<bool>",
250 },
250 },
251 ...
251 ...
252 ]
252 ]
253 error: null
253 error: null
254 """
254 """
255
255
256 include_secrets = has_superadmin_permission(apiuser)
256 include_secrets = has_superadmin_permission(apiuser)
257 _perms = ('repository.read', 'repository.write', 'repository.admin',)
257 _perms = ('repository.read', 'repository.write', 'repository.admin',)
258 extras = {'user': apiuser}
258 extras = {'user': apiuser}
259
259
260 repo_list = RepoList(
260 repo_list = RepoList(
261 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
261 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
262 return [repo.get_api_data(include_secrets=include_secrets)
262 return [repo.get_api_data(include_secrets=include_secrets)
263 for repo in repo_list]
263 for repo in repo_list]
264
264
265
265
266 @jsonrpc_method()
266 @jsonrpc_method()
267 def get_repo_changeset(request, apiuser, repoid, revision,
267 def get_repo_changeset(request, apiuser, repoid, revision,
268 details=Optional('basic')):
268 details=Optional('basic')):
269 """
269 """
270 Returns information about a changeset.
270 Returns information about a changeset.
271
271
272 Additionally parameters define the amount of details returned by
272 Additionally parameters define the amount of details returned by
273 this function.
273 this function.
274
274
275 This command can only be run using an |authtoken| with admin rights,
275 This command can only be run using an |authtoken| with admin rights,
276 or users with at least read rights to the |repo|.
276 or users with at least read rights to the |repo|.
277
277
278 :param apiuser: This is filled automatically from the |authtoken|.
278 :param apiuser: This is filled automatically from the |authtoken|.
279 :type apiuser: AuthUser
279 :type apiuser: AuthUser
280 :param repoid: The repository name or repository id
280 :param repoid: The repository name or repository id
281 :type repoid: str or int
281 :type repoid: str or int
282 :param revision: revision for which listing should be done
282 :param revision: revision for which listing should be done
283 :type revision: str
283 :type revision: str
284 :param details: details can be 'basic|extended|full' full gives diff
284 :param details: details can be 'basic|extended|full' full gives diff
285 info details like the diff itself, and number of changed files etc.
285 info details like the diff itself, and number of changed files etc.
286 :type details: Optional(str)
286 :type details: Optional(str)
287
287
288 """
288 """
289 repo = get_repo_or_error(repoid)
289 repo = get_repo_or_error(repoid)
290 if not has_superadmin_permission(apiuser):
290 if not has_superadmin_permission(apiuser):
291 _perms = (
291 _perms = (
292 'repository.admin', 'repository.write', 'repository.read',)
292 'repository.admin', 'repository.write', 'repository.read',)
293 validate_repo_permissions(apiuser, repoid, repo, _perms)
293 validate_repo_permissions(apiuser, repoid, repo, _perms)
294
294
295 changes_details = Optional.extract(details)
295 changes_details = Optional.extract(details)
296 _changes_details_types = ['basic', 'extended', 'full']
296 _changes_details_types = ['basic', 'extended', 'full']
297 if changes_details not in _changes_details_types:
297 if changes_details not in _changes_details_types:
298 raise JSONRPCError(
298 raise JSONRPCError(
299 'ret_type must be one of %s' % (
299 'ret_type must be one of %s' % (
300 ','.join(_changes_details_types)))
300 ','.join(_changes_details_types)))
301
301
302 pre_load = ['author', 'branch', 'date', 'message', 'parents',
302 pre_load = ['author', 'branch', 'date', 'message', 'parents',
303 'status', '_commit', '_file_paths']
303 'status', '_commit', '_file_paths']
304
304
305 try:
305 try:
306 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
306 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
307 except TypeError as e:
307 except TypeError as e:
308 raise JSONRPCError(e.message)
308 raise JSONRPCError(e.message)
309 _cs_json = cs.__json__()
309 _cs_json = cs.__json__()
310 _cs_json['diff'] = build_commit_data(cs, changes_details)
310 _cs_json['diff'] = build_commit_data(cs, changes_details)
311 if changes_details == 'full':
311 if changes_details == 'full':
312 _cs_json['refs'] = {
312 _cs_json['refs'] = {
313 'branches': [cs.branch],
313 'branches': [cs.branch],
314 'bookmarks': getattr(cs, 'bookmarks', []),
314 'bookmarks': getattr(cs, 'bookmarks', []),
315 'tags': cs.tags
315 'tags': cs.tags
316 }
316 }
317 return _cs_json
317 return _cs_json
318
318
319
319
320 @jsonrpc_method()
320 @jsonrpc_method()
321 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
321 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
322 details=Optional('basic')):
322 details=Optional('basic')):
323 """
323 """
324 Returns a set of commits limited by the number starting
324 Returns a set of commits limited by the number starting
325 from the `start_rev` option.
325 from the `start_rev` option.
326
326
327 Additional parameters define the amount of details returned by this
327 Additional parameters define the amount of details returned by this
328 function.
328 function.
329
329
330 This command can only be run using an |authtoken| with admin rights,
330 This command can only be run using an |authtoken| with admin rights,
331 or users with at least read rights to |repos|.
331 or users with at least read rights to |repos|.
332
332
333 :param apiuser: This is filled automatically from the |authtoken|.
333 :param apiuser: This is filled automatically from the |authtoken|.
334 :type apiuser: AuthUser
334 :type apiuser: AuthUser
335 :param repoid: The repository name or repository ID.
335 :param repoid: The repository name or repository ID.
336 :type repoid: str or int
336 :type repoid: str or int
337 :param start_rev: The starting revision from where to get changesets.
337 :param start_rev: The starting revision from where to get changesets.
338 :type start_rev: str
338 :type start_rev: str
339 :param limit: Limit the number of commits to this amount
339 :param limit: Limit the number of commits to this amount
340 :type limit: str or int
340 :type limit: str or int
341 :param details: Set the level of detail returned. Valid option are:
341 :param details: Set the level of detail returned. Valid option are:
342 ``basic``, ``extended`` and ``full``.
342 ``basic``, ``extended`` and ``full``.
343 :type details: Optional(str)
343 :type details: Optional(str)
344
344
345 .. note::
345 .. note::
346
346
347 Setting the parameter `details` to the value ``full`` is extensive
347 Setting the parameter `details` to the value ``full`` is extensive
348 and returns details like the diff itself, and the number
348 and returns details like the diff itself, and the number
349 of changed files.
349 of changed files.
350
350
351 """
351 """
352 repo = get_repo_or_error(repoid)
352 repo = get_repo_or_error(repoid)
353 if not has_superadmin_permission(apiuser):
353 if not has_superadmin_permission(apiuser):
354 _perms = (
354 _perms = (
355 'repository.admin', 'repository.write', 'repository.read',)
355 'repository.admin', 'repository.write', 'repository.read',)
356 validate_repo_permissions(apiuser, repoid, repo, _perms)
356 validate_repo_permissions(apiuser, repoid, repo, _perms)
357
357
358 changes_details = Optional.extract(details)
358 changes_details = Optional.extract(details)
359 _changes_details_types = ['basic', 'extended', 'full']
359 _changes_details_types = ['basic', 'extended', 'full']
360 if changes_details not in _changes_details_types:
360 if changes_details not in _changes_details_types:
361 raise JSONRPCError(
361 raise JSONRPCError(
362 'ret_type must be one of %s' % (
362 'ret_type must be one of %s' % (
363 ','.join(_changes_details_types)))
363 ','.join(_changes_details_types)))
364
364
365 limit = int(limit)
365 limit = int(limit)
366 pre_load = ['author', 'branch', 'date', 'message', 'parents',
366 pre_load = ['author', 'branch', 'date', 'message', 'parents',
367 'status', '_commit', '_file_paths']
367 'status', '_commit', '_file_paths']
368
368
369 vcs_repo = repo.scm_instance()
369 vcs_repo = repo.scm_instance()
370 # SVN needs a special case to distinguish its index and commit id
370 # SVN needs a special case to distinguish its index and commit id
371 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
371 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
372 start_rev = vcs_repo.commit_ids[0]
372 start_rev = vcs_repo.commit_ids[0]
373
373
374 try:
374 try:
375 commits = vcs_repo.get_commits(
375 commits = vcs_repo.get_commits(
376 start_id=start_rev, pre_load=pre_load)
376 start_id=start_rev, pre_load=pre_load)
377 except TypeError as e:
377 except TypeError as e:
378 raise JSONRPCError(e.message)
378 raise JSONRPCError(e.message)
379 except Exception:
379 except Exception:
380 log.exception('Fetching of commits failed')
380 log.exception('Fetching of commits failed')
381 raise JSONRPCError('Error occurred during commit fetching')
381 raise JSONRPCError('Error occurred during commit fetching')
382
382
383 ret = []
383 ret = []
384 for cnt, commit in enumerate(commits):
384 for cnt, commit in enumerate(commits):
385 if cnt >= limit != -1:
385 if cnt >= limit != -1:
386 break
386 break
387 _cs_json = commit.__json__()
387 _cs_json = commit.__json__()
388 _cs_json['diff'] = build_commit_data(commit, changes_details)
388 _cs_json['diff'] = build_commit_data(commit, changes_details)
389 if changes_details == 'full':
389 if changes_details == 'full':
390 _cs_json['refs'] = {
390 _cs_json['refs'] = {
391 'branches': [commit.branch],
391 'branches': [commit.branch],
392 'bookmarks': getattr(commit, 'bookmarks', []),
392 'bookmarks': getattr(commit, 'bookmarks', []),
393 'tags': commit.tags
393 'tags': commit.tags
394 }
394 }
395 ret.append(_cs_json)
395 ret.append(_cs_json)
396 return ret
396 return ret
397
397
398
398
399 @jsonrpc_method()
399 @jsonrpc_method()
400 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
400 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
401 ret_type=Optional('all'), details=Optional('basic'),
401 ret_type=Optional('all'), details=Optional('basic'),
402 max_file_bytes=Optional(None)):
402 max_file_bytes=Optional(None)):
403 """
403 """
404 Returns a list of nodes and children in a flat list for a given
404 Returns a list of nodes and children in a flat list for a given
405 path at given revision.
405 path at given revision.
406
406
407 It's possible to specify ret_type to show only `files` or `dirs`.
407 It's possible to specify ret_type to show only `files` or `dirs`.
408
408
409 This command can only be run using an |authtoken| with admin rights,
409 This command can only be run using an |authtoken| with admin rights,
410 or users with at least read rights to |repos|.
410 or users with at least read rights to |repos|.
411
411
412 :param apiuser: This is filled automatically from the |authtoken|.
412 :param apiuser: This is filled automatically from the |authtoken|.
413 :type apiuser: AuthUser
413 :type apiuser: AuthUser
414 :param repoid: The repository name or repository ID.
414 :param repoid: The repository name or repository ID.
415 :type repoid: str or int
415 :type repoid: str or int
416 :param revision: The revision for which listing should be done.
416 :param revision: The revision for which listing should be done.
417 :type revision: str
417 :type revision: str
418 :param root_path: The path from which to start displaying.
418 :param root_path: The path from which to start displaying.
419 :type root_path: str
419 :type root_path: str
420 :param ret_type: Set the return type. Valid options are
420 :param ret_type: Set the return type. Valid options are
421 ``all`` (default), ``files`` and ``dirs``.
421 ``all`` (default), ``files`` and ``dirs``.
422 :type ret_type: Optional(str)
422 :type ret_type: Optional(str)
423 :param details: Returns extended information about nodes, such as
423 :param details: Returns extended information about nodes, such as
424 md5, binary, and or content. The valid options are ``basic`` and
424 md5, binary, and or content. The valid options are ``basic`` and
425 ``full``.
425 ``full``.
426 :type details: Optional(str)
426 :type details: Optional(str)
427 :param max_file_bytes: Only return file content under this file size bytes
427 :param max_file_bytes: Only return file content under this file size bytes
428 :type details: Optional(int)
428 :type details: Optional(int)
429
429
430 Example output:
430 Example output:
431
431
432 .. code-block:: bash
432 .. code-block:: bash
433
433
434 id : <id_given_in_input>
434 id : <id_given_in_input>
435 result: [
435 result: [
436 {
436 {
437 "name" : "<name>"
437 "name" : "<name>"
438 "type" : "<type>",
438 "type" : "<type>",
439 "binary": "<true|false>" (only in extended mode)
439 "binary": "<true|false>" (only in extended mode)
440 "md5" : "<md5 of file content>" (only in extended mode)
440 "md5" : "<md5 of file content>" (only in extended mode)
441 },
441 },
442 ...
442 ...
443 ]
443 ]
444 error: null
444 error: null
445 """
445 """
446
446
447 repo = get_repo_or_error(repoid)
447 repo = get_repo_or_error(repoid)
448 if not has_superadmin_permission(apiuser):
448 if not has_superadmin_permission(apiuser):
449 _perms = (
449 _perms = (
450 'repository.admin', 'repository.write', 'repository.read',)
450 'repository.admin', 'repository.write', 'repository.read',)
451 validate_repo_permissions(apiuser, repoid, repo, _perms)
451 validate_repo_permissions(apiuser, repoid, repo, _perms)
452
452
453 ret_type = Optional.extract(ret_type)
453 ret_type = Optional.extract(ret_type)
454 details = Optional.extract(details)
454 details = Optional.extract(details)
455 _extended_types = ['basic', 'full']
455 _extended_types = ['basic', 'full']
456 if details not in _extended_types:
456 if details not in _extended_types:
457 raise JSONRPCError(
457 raise JSONRPCError(
458 'ret_type must be one of %s' % (','.join(_extended_types)))
458 'ret_type must be one of %s' % (','.join(_extended_types)))
459 extended_info = False
459 extended_info = False
460 content = False
460 content = False
461 if details == 'basic':
461 if details == 'basic':
462 extended_info = True
462 extended_info = True
463
463
464 if details == 'full':
464 if details == 'full':
465 extended_info = content = True
465 extended_info = content = True
466
466
467 _map = {}
467 _map = {}
468 try:
468 try:
469 # check if repo is not empty by any chance, skip quicker if it is.
469 # check if repo is not empty by any chance, skip quicker if it is.
470 _scm = repo.scm_instance()
470 _scm = repo.scm_instance()
471 if _scm.is_empty():
471 if _scm.is_empty():
472 return []
472 return []
473
473
474 _d, _f = ScmModel().get_nodes(
474 _d, _f = ScmModel().get_nodes(
475 repo, revision, root_path, flat=False,
475 repo, revision, root_path, flat=False,
476 extended_info=extended_info, content=content,
476 extended_info=extended_info, content=content,
477 max_file_bytes=max_file_bytes)
477 max_file_bytes=max_file_bytes)
478 _map = {
478 _map = {
479 'all': _d + _f,
479 'all': _d + _f,
480 'files': _f,
480 'files': _f,
481 'dirs': _d,
481 'dirs': _d,
482 }
482 }
483 return _map[ret_type]
483 return _map[ret_type]
484 except KeyError:
484 except KeyError:
485 raise JSONRPCError(
485 raise JSONRPCError(
486 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
486 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
487 except Exception:
487 except Exception:
488 log.exception("Exception occurred while trying to get repo nodes")
488 log.exception("Exception occurred while trying to get repo nodes")
489 raise JSONRPCError(
489 raise JSONRPCError(
490 'failed to get repo: `%s` nodes' % repo.repo_name
490 'failed to get repo: `%s` nodes' % repo.repo_name
491 )
491 )
492
492
493
493
494 @jsonrpc_method()
494 @jsonrpc_method()
495 def get_repo_refs(request, apiuser, repoid):
495 def get_repo_refs(request, apiuser, repoid):
496 """
496 """
497 Returns a dictionary of current references. It returns
497 Returns a dictionary of current references. It returns
498 bookmarks, branches, closed_branches, and tags for given repository
498 bookmarks, branches, closed_branches, and tags for given repository
499
499
500 It's possible to specify ret_type to show only `files` or `dirs`.
500 It's possible to specify ret_type to show only `files` or `dirs`.
501
501
502 This command can only be run using an |authtoken| with admin rights,
502 This command can only be run using an |authtoken| with admin rights,
503 or users with at least read rights to |repos|.
503 or users with at least read rights to |repos|.
504
504
505 :param apiuser: This is filled automatically from the |authtoken|.
505 :param apiuser: This is filled automatically from the |authtoken|.
506 :type apiuser: AuthUser
506 :type apiuser: AuthUser
507 :param repoid: The repository name or repository ID.
507 :param repoid: The repository name or repository ID.
508 :type repoid: str or int
508 :type repoid: str or int
509
509
510 Example output:
510 Example output:
511
511
512 .. code-block:: bash
512 .. code-block:: bash
513
513
514 id : <id_given_in_input>
514 id : <id_given_in_input>
515 result: [
515 "result": {
516 TODO...
516 "bookmarks": {
517 ]
517 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
518 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
519 },
520 "branches": {
521 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
522 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
523 },
524 "branches_closed": {},
525 "tags": {
526 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
527 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
528 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
529 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
530 }
531 }
518 error: null
532 error: null
519 """
533 """
520
534
521 repo = get_repo_or_error(repoid)
535 repo = get_repo_or_error(repoid)
522 if not has_superadmin_permission(apiuser):
536 if not has_superadmin_permission(apiuser):
523 _perms = ('repository.admin', 'repository.write', 'repository.read',)
537 _perms = ('repository.admin', 'repository.write', 'repository.read',)
524 validate_repo_permissions(apiuser, repoid, repo, _perms)
538 validate_repo_permissions(apiuser, repoid, repo, _perms)
525
539
526 try:
540 try:
527 # check if repo is not empty by any chance, skip quicker if it is.
541 # check if repo is not empty by any chance, skip quicker if it is.
528 vcs_instance = repo.scm_instance()
542 vcs_instance = repo.scm_instance()
529 refs = vcs_instance.refs()
543 refs = vcs_instance.refs()
530 return refs
544 return refs
531 except Exception:
545 except Exception:
532 log.exception("Exception occurred while trying to get repo refs")
546 log.exception("Exception occurred while trying to get repo refs")
533 raise JSONRPCError(
547 raise JSONRPCError(
534 'failed to get repo: `%s` references' % repo.repo_name
548 'failed to get repo: `%s` references' % repo.repo_name
535 )
549 )
536
550
537
551
538 @jsonrpc_method()
552 @jsonrpc_method()
539 def create_repo(
553 def create_repo(
540 request, apiuser, repo_name, repo_type,
554 request, apiuser, repo_name, repo_type,
541 owner=Optional(OAttr('apiuser')),
555 owner=Optional(OAttr('apiuser')),
542 description=Optional(''),
556 description=Optional(''),
543 private=Optional(False),
557 private=Optional(False),
544 clone_uri=Optional(None),
558 clone_uri=Optional(None),
545 landing_rev=Optional('rev:tip'),
559 landing_rev=Optional('rev:tip'),
546 enable_statistics=Optional(False),
560 enable_statistics=Optional(False),
547 enable_locking=Optional(False),
561 enable_locking=Optional(False),
548 enable_downloads=Optional(False),
562 enable_downloads=Optional(False),
549 copy_permissions=Optional(False)):
563 copy_permissions=Optional(False)):
550 """
564 """
551 Creates a repository.
565 Creates a repository.
552
566
553 * If the repository name contains "/", repository will be created inside
567 * If the repository name contains "/", repository will be created inside
554 a repository group or nested repository groups
568 a repository group or nested repository groups
555
569
556 For example "foo/bar/repo1" will create |repo| called "repo1" inside
570 For example "foo/bar/repo1" will create |repo| called "repo1" inside
557 group "foo/bar". You have to have permissions to access and write to
571 group "foo/bar". You have to have permissions to access and write to
558 the last repository group ("bar" in this example)
572 the last repository group ("bar" in this example)
559
573
560 This command can only be run using an |authtoken| with at least
574 This command can only be run using an |authtoken| with at least
561 permissions to create repositories, or write permissions to
575 permissions to create repositories, or write permissions to
562 parent repository groups.
576 parent repository groups.
563
577
564 :param apiuser: This is filled automatically from the |authtoken|.
578 :param apiuser: This is filled automatically from the |authtoken|.
565 :type apiuser: AuthUser
579 :type apiuser: AuthUser
566 :param repo_name: Set the repository name.
580 :param repo_name: Set the repository name.
567 :type repo_name: str
581 :type repo_name: str
568 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
582 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
569 :type repo_type: str
583 :type repo_type: str
570 :param owner: user_id or username
584 :param owner: user_id or username
571 :type owner: Optional(str)
585 :type owner: Optional(str)
572 :param description: Set the repository description.
586 :param description: Set the repository description.
573 :type description: Optional(str)
587 :type description: Optional(str)
574 :param private: set repository as private
588 :param private: set repository as private
575 :type private: bool
589 :type private: bool
576 :param clone_uri: set clone_uri
590 :param clone_uri: set clone_uri
577 :type clone_uri: str
591 :type clone_uri: str
578 :param landing_rev: <rev_type>:<rev>
592 :param landing_rev: <rev_type>:<rev>
579 :type landing_rev: str
593 :type landing_rev: str
580 :param enable_locking:
594 :param enable_locking:
581 :type enable_locking: bool
595 :type enable_locking: bool
582 :param enable_downloads:
596 :param enable_downloads:
583 :type enable_downloads: bool
597 :type enable_downloads: bool
584 :param enable_statistics:
598 :param enable_statistics:
585 :type enable_statistics: bool
599 :type enable_statistics: bool
586 :param copy_permissions: Copy permission from group in which the
600 :param copy_permissions: Copy permission from group in which the
587 repository is being created.
601 repository is being created.
588 :type copy_permissions: bool
602 :type copy_permissions: bool
589
603
590
604
591 Example output:
605 Example output:
592
606
593 .. code-block:: bash
607 .. code-block:: bash
594
608
595 id : <id_given_in_input>
609 id : <id_given_in_input>
596 result: {
610 result: {
597 "msg": "Created new repository `<reponame>`",
611 "msg": "Created new repository `<reponame>`",
598 "success": true,
612 "success": true,
599 "task": "<celery task id or None if done sync>"
613 "task": "<celery task id or None if done sync>"
600 }
614 }
601 error: null
615 error: null
602
616
603
617
604 Example error output:
618 Example error output:
605
619
606 .. code-block:: bash
620 .. code-block:: bash
607
621
608 id : <id_given_in_input>
622 id : <id_given_in_input>
609 result : null
623 result : null
610 error : {
624 error : {
611 'failed to create repository `<repo_name>`'
625 'failed to create repository `<repo_name>`'
612 }
626 }
613
627
614 """
628 """
615
629
616 owner = validate_set_owner_permissions(apiuser, owner)
630 owner = validate_set_owner_permissions(apiuser, owner)
617
631
618 description = Optional.extract(description)
632 description = Optional.extract(description)
619 copy_permissions = Optional.extract(copy_permissions)
633 copy_permissions = Optional.extract(copy_permissions)
620 clone_uri = Optional.extract(clone_uri)
634 clone_uri = Optional.extract(clone_uri)
621 landing_commit_ref = Optional.extract(landing_rev)
635 landing_commit_ref = Optional.extract(landing_rev)
622
636
623 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
637 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
624 if isinstance(private, Optional):
638 if isinstance(private, Optional):
625 private = defs.get('repo_private') or Optional.extract(private)
639 private = defs.get('repo_private') or Optional.extract(private)
626 if isinstance(repo_type, Optional):
640 if isinstance(repo_type, Optional):
627 repo_type = defs.get('repo_type')
641 repo_type = defs.get('repo_type')
628 if isinstance(enable_statistics, Optional):
642 if isinstance(enable_statistics, Optional):
629 enable_statistics = defs.get('repo_enable_statistics')
643 enable_statistics = defs.get('repo_enable_statistics')
630 if isinstance(enable_locking, Optional):
644 if isinstance(enable_locking, Optional):
631 enable_locking = defs.get('repo_enable_locking')
645 enable_locking = defs.get('repo_enable_locking')
632 if isinstance(enable_downloads, Optional):
646 if isinstance(enable_downloads, Optional):
633 enable_downloads = defs.get('repo_enable_downloads')
647 enable_downloads = defs.get('repo_enable_downloads')
634
648
635 schema = repo_schema.RepoSchema().bind(
649 schema = repo_schema.RepoSchema().bind(
636 repo_type_options=rhodecode.BACKENDS.keys(),
650 repo_type_options=rhodecode.BACKENDS.keys(),
637 # user caller
651 # user caller
638 user=apiuser)
652 user=apiuser)
639
653
640 try:
654 try:
641 schema_data = schema.deserialize(dict(
655 schema_data = schema.deserialize(dict(
642 repo_name=repo_name,
656 repo_name=repo_name,
643 repo_type=repo_type,
657 repo_type=repo_type,
644 repo_owner=owner.username,
658 repo_owner=owner.username,
645 repo_description=description,
659 repo_description=description,
646 repo_landing_commit_ref=landing_commit_ref,
660 repo_landing_commit_ref=landing_commit_ref,
647 repo_clone_uri=clone_uri,
661 repo_clone_uri=clone_uri,
648 repo_private=private,
662 repo_private=private,
649 repo_copy_permissions=copy_permissions,
663 repo_copy_permissions=copy_permissions,
650 repo_enable_statistics=enable_statistics,
664 repo_enable_statistics=enable_statistics,
651 repo_enable_downloads=enable_downloads,
665 repo_enable_downloads=enable_downloads,
652 repo_enable_locking=enable_locking))
666 repo_enable_locking=enable_locking))
653 except validation_schema.Invalid as err:
667 except validation_schema.Invalid as err:
654 raise JSONRPCValidationError(colander_exc=err)
668 raise JSONRPCValidationError(colander_exc=err)
655
669
656 try:
670 try:
657 data = {
671 data = {
658 'owner': owner,
672 'owner': owner,
659 'repo_name': schema_data['repo_group']['repo_name_without_group'],
673 'repo_name': schema_data['repo_group']['repo_name_without_group'],
660 'repo_name_full': schema_data['repo_name'],
674 'repo_name_full': schema_data['repo_name'],
661 'repo_group': schema_data['repo_group']['repo_group_id'],
675 'repo_group': schema_data['repo_group']['repo_group_id'],
662 'repo_type': schema_data['repo_type'],
676 'repo_type': schema_data['repo_type'],
663 'repo_description': schema_data['repo_description'],
677 'repo_description': schema_data['repo_description'],
664 'repo_private': schema_data['repo_private'],
678 'repo_private': schema_data['repo_private'],
665 'clone_uri': schema_data['repo_clone_uri'],
679 'clone_uri': schema_data['repo_clone_uri'],
666 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
680 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
667 'enable_statistics': schema_data['repo_enable_statistics'],
681 'enable_statistics': schema_data['repo_enable_statistics'],
668 'enable_locking': schema_data['repo_enable_locking'],
682 'enable_locking': schema_data['repo_enable_locking'],
669 'enable_downloads': schema_data['repo_enable_downloads'],
683 'enable_downloads': schema_data['repo_enable_downloads'],
670 'repo_copy_permissions': schema_data['repo_copy_permissions'],
684 'repo_copy_permissions': schema_data['repo_copy_permissions'],
671 }
685 }
672
686
673 task = RepoModel().create(form_data=data, cur_user=owner)
687 task = RepoModel().create(form_data=data, cur_user=owner)
674 from celery.result import BaseAsyncResult
688 from celery.result import BaseAsyncResult
675 task_id = None
689 task_id = None
676 if isinstance(task, BaseAsyncResult):
690 if isinstance(task, BaseAsyncResult):
677 task_id = task.task_id
691 task_id = task.task_id
678 # no commit, it's done in RepoModel, or async via celery
692 # no commit, it's done in RepoModel, or async via celery
679 return {
693 return {
680 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
694 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
681 'success': True, # cannot return the repo data here since fork
695 'success': True, # cannot return the repo data here since fork
682 # can be done async
696 # can be done async
683 'task': task_id
697 'task': task_id
684 }
698 }
685 except Exception:
699 except Exception:
686 log.exception(
700 log.exception(
687 u"Exception while trying to create the repository %s",
701 u"Exception while trying to create the repository %s",
688 schema_data['repo_name'])
702 schema_data['repo_name'])
689 raise JSONRPCError(
703 raise JSONRPCError(
690 'failed to create repository `%s`' % (schema_data['repo_name'],))
704 'failed to create repository `%s`' % (schema_data['repo_name'],))
691
705
692
706
693 @jsonrpc_method()
707 @jsonrpc_method()
694 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
708 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
695 description=Optional('')):
709 description=Optional('')):
696 """
710 """
697 Adds an extra field to a repository.
711 Adds an extra field to a repository.
698
712
699 This command can only be run using an |authtoken| with at least
713 This command can only be run using an |authtoken| with at least
700 write permissions to the |repo|.
714 write permissions to the |repo|.
701
715
702 :param apiuser: This is filled automatically from the |authtoken|.
716 :param apiuser: This is filled automatically from the |authtoken|.
703 :type apiuser: AuthUser
717 :type apiuser: AuthUser
704 :param repoid: Set the repository name or repository id.
718 :param repoid: Set the repository name or repository id.
705 :type repoid: str or int
719 :type repoid: str or int
706 :param key: Create a unique field key for this repository.
720 :param key: Create a unique field key for this repository.
707 :type key: str
721 :type key: str
708 :param label:
722 :param label:
709 :type label: Optional(str)
723 :type label: Optional(str)
710 :param description:
724 :param description:
711 :type description: Optional(str)
725 :type description: Optional(str)
712 """
726 """
713 repo = get_repo_or_error(repoid)
727 repo = get_repo_or_error(repoid)
714 if not has_superadmin_permission(apiuser):
728 if not has_superadmin_permission(apiuser):
715 _perms = ('repository.admin',)
729 _perms = ('repository.admin',)
716 validate_repo_permissions(apiuser, repoid, repo, _perms)
730 validate_repo_permissions(apiuser, repoid, repo, _perms)
717
731
718 label = Optional.extract(label) or key
732 label = Optional.extract(label) or key
719 description = Optional.extract(description)
733 description = Optional.extract(description)
720
734
721 field = RepositoryField.get_by_key_name(key, repo)
735 field = RepositoryField.get_by_key_name(key, repo)
722 if field:
736 if field:
723 raise JSONRPCError('Field with key '
737 raise JSONRPCError('Field with key '
724 '`%s` exists for repo `%s`' % (key, repoid))
738 '`%s` exists for repo `%s`' % (key, repoid))
725
739
726 try:
740 try:
727 RepoModel().add_repo_field(repo, key, field_label=label,
741 RepoModel().add_repo_field(repo, key, field_label=label,
728 field_desc=description)
742 field_desc=description)
729 Session().commit()
743 Session().commit()
730 return {
744 return {
731 'msg': "Added new repository field `%s`" % (key,),
745 'msg': "Added new repository field `%s`" % (key,),
732 'success': True,
746 'success': True,
733 }
747 }
734 except Exception:
748 except Exception:
735 log.exception("Exception occurred while trying to add field to repo")
749 log.exception("Exception occurred while trying to add field to repo")
736 raise JSONRPCError(
750 raise JSONRPCError(
737 'failed to create new field for repository `%s`' % (repoid,))
751 'failed to create new field for repository `%s`' % (repoid,))
738
752
739
753
740 @jsonrpc_method()
754 @jsonrpc_method()
741 def remove_field_from_repo(request, apiuser, repoid, key):
755 def remove_field_from_repo(request, apiuser, repoid, key):
742 """
756 """
743 Removes an extra field from a repository.
757 Removes an extra field from a repository.
744
758
745 This command can only be run using an |authtoken| with at least
759 This command can only be run using an |authtoken| with at least
746 write permissions to the |repo|.
760 write permissions to the |repo|.
747
761
748 :param apiuser: This is filled automatically from the |authtoken|.
762 :param apiuser: This is filled automatically from the |authtoken|.
749 :type apiuser: AuthUser
763 :type apiuser: AuthUser
750 :param repoid: Set the repository name or repository ID.
764 :param repoid: Set the repository name or repository ID.
751 :type repoid: str or int
765 :type repoid: str or int
752 :param key: Set the unique field key for this repository.
766 :param key: Set the unique field key for this repository.
753 :type key: str
767 :type key: str
754 """
768 """
755
769
756 repo = get_repo_or_error(repoid)
770 repo = get_repo_or_error(repoid)
757 if not has_superadmin_permission(apiuser):
771 if not has_superadmin_permission(apiuser):
758 _perms = ('repository.admin',)
772 _perms = ('repository.admin',)
759 validate_repo_permissions(apiuser, repoid, repo, _perms)
773 validate_repo_permissions(apiuser, repoid, repo, _perms)
760
774
761 field = RepositoryField.get_by_key_name(key, repo)
775 field = RepositoryField.get_by_key_name(key, repo)
762 if not field:
776 if not field:
763 raise JSONRPCError('Field with key `%s` does not '
777 raise JSONRPCError('Field with key `%s` does not '
764 'exists for repo `%s`' % (key, repoid))
778 'exists for repo `%s`' % (key, repoid))
765
779
766 try:
780 try:
767 RepoModel().delete_repo_field(repo, field_key=key)
781 RepoModel().delete_repo_field(repo, field_key=key)
768 Session().commit()
782 Session().commit()
769 return {
783 return {
770 'msg': "Deleted repository field `%s`" % (key,),
784 'msg': "Deleted repository field `%s`" % (key,),
771 'success': True,
785 'success': True,
772 }
786 }
773 except Exception:
787 except Exception:
774 log.exception(
788 log.exception(
775 "Exception occurred while trying to delete field from repo")
789 "Exception occurred while trying to delete field from repo")
776 raise JSONRPCError(
790 raise JSONRPCError(
777 'failed to delete field for repository `%s`' % (repoid,))
791 'failed to delete field for repository `%s`' % (repoid,))
778
792
779
793
780 @jsonrpc_method()
794 @jsonrpc_method()
781 def update_repo(
795 def update_repo(
782 request, apiuser, repoid, repo_name=Optional(None),
796 request, apiuser, repoid, repo_name=Optional(None),
783 owner=Optional(OAttr('apiuser')), description=Optional(''),
797 owner=Optional(OAttr('apiuser')), description=Optional(''),
784 private=Optional(False), clone_uri=Optional(None),
798 private=Optional(False), clone_uri=Optional(None),
785 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
799 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
786 enable_statistics=Optional(False),
800 enable_statistics=Optional(False),
787 enable_locking=Optional(False),
801 enable_locking=Optional(False),
788 enable_downloads=Optional(False), fields=Optional('')):
802 enable_downloads=Optional(False), fields=Optional('')):
789 """
803 """
790 Updates a repository with the given information.
804 Updates a repository with the given information.
791
805
792 This command can only be run using an |authtoken| with at least
806 This command can only be run using an |authtoken| with at least
793 admin permissions to the |repo|.
807 admin permissions to the |repo|.
794
808
795 * If the repository name contains "/", repository will be updated
809 * If the repository name contains "/", repository will be updated
796 accordingly with a repository group or nested repository groups
810 accordingly with a repository group or nested repository groups
797
811
798 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
812 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
799 called "repo-test" and place it inside group "foo/bar".
813 called "repo-test" and place it inside group "foo/bar".
800 You have to have permissions to access and write to the last repository
814 You have to have permissions to access and write to the last repository
801 group ("bar" in this example)
815 group ("bar" in this example)
802
816
803 :param apiuser: This is filled automatically from the |authtoken|.
817 :param apiuser: This is filled automatically from the |authtoken|.
804 :type apiuser: AuthUser
818 :type apiuser: AuthUser
805 :param repoid: repository name or repository ID.
819 :param repoid: repository name or repository ID.
806 :type repoid: str or int
820 :type repoid: str or int
807 :param repo_name: Update the |repo| name, including the
821 :param repo_name: Update the |repo| name, including the
808 repository group it's in.
822 repository group it's in.
809 :type repo_name: str
823 :type repo_name: str
810 :param owner: Set the |repo| owner.
824 :param owner: Set the |repo| owner.
811 :type owner: str
825 :type owner: str
812 :param fork_of: Set the |repo| as fork of another |repo|.
826 :param fork_of: Set the |repo| as fork of another |repo|.
813 :type fork_of: str
827 :type fork_of: str
814 :param description: Update the |repo| description.
828 :param description: Update the |repo| description.
815 :type description: str
829 :type description: str
816 :param private: Set the |repo| as private. (True | False)
830 :param private: Set the |repo| as private. (True | False)
817 :type private: bool
831 :type private: bool
818 :param clone_uri: Update the |repo| clone URI.
832 :param clone_uri: Update the |repo| clone URI.
819 :type clone_uri: str
833 :type clone_uri: str
820 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
834 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
821 :type landing_rev: str
835 :type landing_rev: str
822 :param enable_statistics: Enable statistics on the |repo|, (True | False).
836 :param enable_statistics: Enable statistics on the |repo|, (True | False).
823 :type enable_statistics: bool
837 :type enable_statistics: bool
824 :param enable_locking: Enable |repo| locking.
838 :param enable_locking: Enable |repo| locking.
825 :type enable_locking: bool
839 :type enable_locking: bool
826 :param enable_downloads: Enable downloads from the |repo|, (True | False).
840 :param enable_downloads: Enable downloads from the |repo|, (True | False).
827 :type enable_downloads: bool
841 :type enable_downloads: bool
828 :param fields: Add extra fields to the |repo|. Use the following
842 :param fields: Add extra fields to the |repo|. Use the following
829 example format: ``field_key=field_val,field_key2=fieldval2``.
843 example format: ``field_key=field_val,field_key2=fieldval2``.
830 Escape ', ' with \,
844 Escape ', ' with \,
831 :type fields: str
845 :type fields: str
832 """
846 """
833
847
834 repo = get_repo_or_error(repoid)
848 repo = get_repo_or_error(repoid)
835
849
836 include_secrets = False
850 include_secrets = False
837 if not has_superadmin_permission(apiuser):
851 if not has_superadmin_permission(apiuser):
838 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
852 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
839 else:
853 else:
840 include_secrets = True
854 include_secrets = True
841
855
842 updates = dict(
856 updates = dict(
843 repo_name=repo_name
857 repo_name=repo_name
844 if not isinstance(repo_name, Optional) else repo.repo_name,
858 if not isinstance(repo_name, Optional) else repo.repo_name,
845
859
846 fork_id=fork_of
860 fork_id=fork_of
847 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
861 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
848
862
849 user=owner
863 user=owner
850 if not isinstance(owner, Optional) else repo.user.username,
864 if not isinstance(owner, Optional) else repo.user.username,
851
865
852 repo_description=description
866 repo_description=description
853 if not isinstance(description, Optional) else repo.description,
867 if not isinstance(description, Optional) else repo.description,
854
868
855 repo_private=private
869 repo_private=private
856 if not isinstance(private, Optional) else repo.private,
870 if not isinstance(private, Optional) else repo.private,
857
871
858 clone_uri=clone_uri
872 clone_uri=clone_uri
859 if not isinstance(clone_uri, Optional) else repo.clone_uri,
873 if not isinstance(clone_uri, Optional) else repo.clone_uri,
860
874
861 repo_landing_rev=landing_rev
875 repo_landing_rev=landing_rev
862 if not isinstance(landing_rev, Optional) else repo._landing_revision,
876 if not isinstance(landing_rev, Optional) else repo._landing_revision,
863
877
864 repo_enable_statistics=enable_statistics
878 repo_enable_statistics=enable_statistics
865 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
879 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
866
880
867 repo_enable_locking=enable_locking
881 repo_enable_locking=enable_locking
868 if not isinstance(enable_locking, Optional) else repo.enable_locking,
882 if not isinstance(enable_locking, Optional) else repo.enable_locking,
869
883
870 repo_enable_downloads=enable_downloads
884 repo_enable_downloads=enable_downloads
871 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
885 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
872
886
873 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
887 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
874
888
875 schema = repo_schema.RepoSchema().bind(
889 schema = repo_schema.RepoSchema().bind(
876 repo_type_options=rhodecode.BACKENDS.keys(),
890 repo_type_options=rhodecode.BACKENDS.keys(),
877 repo_ref_options=ref_choices,
891 repo_ref_options=ref_choices,
878 # user caller
892 # user caller
879 user=apiuser,
893 user=apiuser,
880 old_values=repo.get_api_data())
894 old_values=repo.get_api_data())
881 try:
895 try:
882 schema_data = schema.deserialize(dict(
896 schema_data = schema.deserialize(dict(
883 # we save old value, users cannot change type
897 # we save old value, users cannot change type
884 repo_type=repo.repo_type,
898 repo_type=repo.repo_type,
885
899
886 repo_name=updates['repo_name'],
900 repo_name=updates['repo_name'],
887 repo_owner=updates['user'],
901 repo_owner=updates['user'],
888 repo_description=updates['repo_description'],
902 repo_description=updates['repo_description'],
889 repo_clone_uri=updates['clone_uri'],
903 repo_clone_uri=updates['clone_uri'],
890 repo_fork_of=updates['fork_id'],
904 repo_fork_of=updates['fork_id'],
891 repo_private=updates['repo_private'],
905 repo_private=updates['repo_private'],
892 repo_landing_commit_ref=updates['repo_landing_rev'],
906 repo_landing_commit_ref=updates['repo_landing_rev'],
893 repo_enable_statistics=updates['repo_enable_statistics'],
907 repo_enable_statistics=updates['repo_enable_statistics'],
894 repo_enable_downloads=updates['repo_enable_downloads'],
908 repo_enable_downloads=updates['repo_enable_downloads'],
895 repo_enable_locking=updates['repo_enable_locking']))
909 repo_enable_locking=updates['repo_enable_locking']))
896 except validation_schema.Invalid as err:
910 except validation_schema.Invalid as err:
897 raise JSONRPCValidationError(colander_exc=err)
911 raise JSONRPCValidationError(colander_exc=err)
898
912
899 # save validated data back into the updates dict
913 # save validated data back into the updates dict
900 validated_updates = dict(
914 validated_updates = dict(
901 repo_name=schema_data['repo_group']['repo_name_without_group'],
915 repo_name=schema_data['repo_group']['repo_name_without_group'],
902 repo_group=schema_data['repo_group']['repo_group_id'],
916 repo_group=schema_data['repo_group']['repo_group_id'],
903
917
904 user=schema_data['repo_owner'],
918 user=schema_data['repo_owner'],
905 repo_description=schema_data['repo_description'],
919 repo_description=schema_data['repo_description'],
906 repo_private=schema_data['repo_private'],
920 repo_private=schema_data['repo_private'],
907 clone_uri=schema_data['repo_clone_uri'],
921 clone_uri=schema_data['repo_clone_uri'],
908 repo_landing_rev=schema_data['repo_landing_commit_ref'],
922 repo_landing_rev=schema_data['repo_landing_commit_ref'],
909 repo_enable_statistics=schema_data['repo_enable_statistics'],
923 repo_enable_statistics=schema_data['repo_enable_statistics'],
910 repo_enable_locking=schema_data['repo_enable_locking'],
924 repo_enable_locking=schema_data['repo_enable_locking'],
911 repo_enable_downloads=schema_data['repo_enable_downloads'],
925 repo_enable_downloads=schema_data['repo_enable_downloads'],
912 )
926 )
913
927
914 if schema_data['repo_fork_of']:
928 if schema_data['repo_fork_of']:
915 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
929 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
916 validated_updates['fork_id'] = fork_repo.repo_id
930 validated_updates['fork_id'] = fork_repo.repo_id
917
931
918 # extra fields
932 # extra fields
919 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
933 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
920 if fields:
934 if fields:
921 validated_updates.update(fields)
935 validated_updates.update(fields)
922
936
923 try:
937 try:
924 RepoModel().update(repo, **validated_updates)
938 RepoModel().update(repo, **validated_updates)
925 Session().commit()
939 Session().commit()
926 return {
940 return {
927 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
941 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
928 'repository': repo.get_api_data(include_secrets=include_secrets)
942 'repository': repo.get_api_data(include_secrets=include_secrets)
929 }
943 }
930 except Exception:
944 except Exception:
931 log.exception(
945 log.exception(
932 u"Exception while trying to update the repository %s",
946 u"Exception while trying to update the repository %s",
933 repoid)
947 repoid)
934 raise JSONRPCError('failed to update repo `%s`' % repoid)
948 raise JSONRPCError('failed to update repo `%s`' % repoid)
935
949
936
950
937 @jsonrpc_method()
951 @jsonrpc_method()
938 def fork_repo(request, apiuser, repoid, fork_name,
952 def fork_repo(request, apiuser, repoid, fork_name,
939 owner=Optional(OAttr('apiuser')),
953 owner=Optional(OAttr('apiuser')),
940 description=Optional(''),
954 description=Optional(''),
941 private=Optional(False),
955 private=Optional(False),
942 clone_uri=Optional(None),
956 clone_uri=Optional(None),
943 landing_rev=Optional('rev:tip'),
957 landing_rev=Optional('rev:tip'),
944 copy_permissions=Optional(False)):
958 copy_permissions=Optional(False)):
945 """
959 """
946 Creates a fork of the specified |repo|.
960 Creates a fork of the specified |repo|.
947
961
948 * If the fork_name contains "/", fork will be created inside
962 * If the fork_name contains "/", fork will be created inside
949 a repository group or nested repository groups
963 a repository group or nested repository groups
950
964
951 For example "foo/bar/fork-repo" will create fork called "fork-repo"
965 For example "foo/bar/fork-repo" will create fork called "fork-repo"
952 inside group "foo/bar". You have to have permissions to access and
966 inside group "foo/bar". You have to have permissions to access and
953 write to the last repository group ("bar" in this example)
967 write to the last repository group ("bar" in this example)
954
968
955 This command can only be run using an |authtoken| with minimum
969 This command can only be run using an |authtoken| with minimum
956 read permissions of the forked repo, create fork permissions for an user.
970 read permissions of the forked repo, create fork permissions for an user.
957
971
958 :param apiuser: This is filled automatically from the |authtoken|.
972 :param apiuser: This is filled automatically from the |authtoken|.
959 :type apiuser: AuthUser
973 :type apiuser: AuthUser
960 :param repoid: Set repository name or repository ID.
974 :param repoid: Set repository name or repository ID.
961 :type repoid: str or int
975 :type repoid: str or int
962 :param fork_name: Set the fork name, including it's repository group membership.
976 :param fork_name: Set the fork name, including it's repository group membership.
963 :type fork_name: str
977 :type fork_name: str
964 :param owner: Set the fork owner.
978 :param owner: Set the fork owner.
965 :type owner: str
979 :type owner: str
966 :param description: Set the fork description.
980 :param description: Set the fork description.
967 :type description: str
981 :type description: str
968 :param copy_permissions: Copy permissions from parent |repo|. The
982 :param copy_permissions: Copy permissions from parent |repo|. The
969 default is False.
983 default is False.
970 :type copy_permissions: bool
984 :type copy_permissions: bool
971 :param private: Make the fork private. The default is False.
985 :param private: Make the fork private. The default is False.
972 :type private: bool
986 :type private: bool
973 :param landing_rev: Set the landing revision. The default is tip.
987 :param landing_rev: Set the landing revision. The default is tip.
974
988
975 Example output:
989 Example output:
976
990
977 .. code-block:: bash
991 .. code-block:: bash
978
992
979 id : <id_for_response>
993 id : <id_for_response>
980 api_key : "<api_key>"
994 api_key : "<api_key>"
981 args: {
995 args: {
982 "repoid" : "<reponame or repo_id>",
996 "repoid" : "<reponame or repo_id>",
983 "fork_name": "<forkname>",
997 "fork_name": "<forkname>",
984 "owner": "<username or user_id = Optional(=apiuser)>",
998 "owner": "<username or user_id = Optional(=apiuser)>",
985 "description": "<description>",
999 "description": "<description>",
986 "copy_permissions": "<bool>",
1000 "copy_permissions": "<bool>",
987 "private": "<bool>",
1001 "private": "<bool>",
988 "landing_rev": "<landing_rev>"
1002 "landing_rev": "<landing_rev>"
989 }
1003 }
990
1004
991 Example error output:
1005 Example error output:
992
1006
993 .. code-block:: bash
1007 .. code-block:: bash
994
1008
995 id : <id_given_in_input>
1009 id : <id_given_in_input>
996 result: {
1010 result: {
997 "msg": "Created fork of `<reponame>` as `<forkname>`",
1011 "msg": "Created fork of `<reponame>` as `<forkname>`",
998 "success": true,
1012 "success": true,
999 "task": "<celery task id or None if done sync>"
1013 "task": "<celery task id or None if done sync>"
1000 }
1014 }
1001 error: null
1015 error: null
1002
1016
1003 """
1017 """
1004
1018
1005 repo = get_repo_or_error(repoid)
1019 repo = get_repo_or_error(repoid)
1006 repo_name = repo.repo_name
1020 repo_name = repo.repo_name
1007
1021
1008 if not has_superadmin_permission(apiuser):
1022 if not has_superadmin_permission(apiuser):
1009 # check if we have at least read permission for
1023 # check if we have at least read permission for
1010 # this repo that we fork !
1024 # this repo that we fork !
1011 _perms = (
1025 _perms = (
1012 'repository.admin', 'repository.write', 'repository.read')
1026 'repository.admin', 'repository.write', 'repository.read')
1013 validate_repo_permissions(apiuser, repoid, repo, _perms)
1027 validate_repo_permissions(apiuser, repoid, repo, _perms)
1014
1028
1015 # check if the regular user has at least fork permissions as well
1029 # check if the regular user has at least fork permissions as well
1016 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1030 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1017 raise JSONRPCForbidden()
1031 raise JSONRPCForbidden()
1018
1032
1019 # check if user can set owner parameter
1033 # check if user can set owner parameter
1020 owner = validate_set_owner_permissions(apiuser, owner)
1034 owner = validate_set_owner_permissions(apiuser, owner)
1021
1035
1022 description = Optional.extract(description)
1036 description = Optional.extract(description)
1023 copy_permissions = Optional.extract(copy_permissions)
1037 copy_permissions = Optional.extract(copy_permissions)
1024 clone_uri = Optional.extract(clone_uri)
1038 clone_uri = Optional.extract(clone_uri)
1025 landing_commit_ref = Optional.extract(landing_rev)
1039 landing_commit_ref = Optional.extract(landing_rev)
1026 private = Optional.extract(private)
1040 private = Optional.extract(private)
1027
1041
1028 schema = repo_schema.RepoSchema().bind(
1042 schema = repo_schema.RepoSchema().bind(
1029 repo_type_options=rhodecode.BACKENDS.keys(),
1043 repo_type_options=rhodecode.BACKENDS.keys(),
1030 # user caller
1044 # user caller
1031 user=apiuser)
1045 user=apiuser)
1032
1046
1033 try:
1047 try:
1034 schema_data = schema.deserialize(dict(
1048 schema_data = schema.deserialize(dict(
1035 repo_name=fork_name,
1049 repo_name=fork_name,
1036 repo_type=repo.repo_type,
1050 repo_type=repo.repo_type,
1037 repo_owner=owner.username,
1051 repo_owner=owner.username,
1038 repo_description=description,
1052 repo_description=description,
1039 repo_landing_commit_ref=landing_commit_ref,
1053 repo_landing_commit_ref=landing_commit_ref,
1040 repo_clone_uri=clone_uri,
1054 repo_clone_uri=clone_uri,
1041 repo_private=private,
1055 repo_private=private,
1042 repo_copy_permissions=copy_permissions))
1056 repo_copy_permissions=copy_permissions))
1043 except validation_schema.Invalid as err:
1057 except validation_schema.Invalid as err:
1044 raise JSONRPCValidationError(colander_exc=err)
1058 raise JSONRPCValidationError(colander_exc=err)
1045
1059
1046 try:
1060 try:
1047 data = {
1061 data = {
1048 'fork_parent_id': repo.repo_id,
1062 'fork_parent_id': repo.repo_id,
1049
1063
1050 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1064 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1051 'repo_name_full': schema_data['repo_name'],
1065 'repo_name_full': schema_data['repo_name'],
1052 'repo_group': schema_data['repo_group']['repo_group_id'],
1066 'repo_group': schema_data['repo_group']['repo_group_id'],
1053 'repo_type': schema_data['repo_type'],
1067 'repo_type': schema_data['repo_type'],
1054 'description': schema_data['repo_description'],
1068 'description': schema_data['repo_description'],
1055 'private': schema_data['repo_private'],
1069 'private': schema_data['repo_private'],
1056 'copy_permissions': schema_data['repo_copy_permissions'],
1070 'copy_permissions': schema_data['repo_copy_permissions'],
1057 'landing_rev': schema_data['repo_landing_commit_ref'],
1071 'landing_rev': schema_data['repo_landing_commit_ref'],
1058 }
1072 }
1059
1073
1060 task = RepoModel().create_fork(data, cur_user=owner)
1074 task = RepoModel().create_fork(data, cur_user=owner)
1061 # no commit, it's done in RepoModel, or async via celery
1075 # no commit, it's done in RepoModel, or async via celery
1062 from celery.result import BaseAsyncResult
1076 from celery.result import BaseAsyncResult
1063 task_id = None
1077 task_id = None
1064 if isinstance(task, BaseAsyncResult):
1078 if isinstance(task, BaseAsyncResult):
1065 task_id = task.task_id
1079 task_id = task.task_id
1066 return {
1080 return {
1067 'msg': 'Created fork of `%s` as `%s`' % (
1081 'msg': 'Created fork of `%s` as `%s`' % (
1068 repo.repo_name, schema_data['repo_name']),
1082 repo.repo_name, schema_data['repo_name']),
1069 'success': True, # cannot return the repo data here since fork
1083 'success': True, # cannot return the repo data here since fork
1070 # can be done async
1084 # can be done async
1071 'task': task_id
1085 'task': task_id
1072 }
1086 }
1073 except Exception:
1087 except Exception:
1074 log.exception(
1088 log.exception(
1075 u"Exception while trying to create fork %s",
1089 u"Exception while trying to create fork %s",
1076 schema_data['repo_name'])
1090 schema_data['repo_name'])
1077 raise JSONRPCError(
1091 raise JSONRPCError(
1078 'failed to fork repository `%s` as `%s`' % (
1092 'failed to fork repository `%s` as `%s`' % (
1079 repo_name, schema_data['repo_name']))
1093 repo_name, schema_data['repo_name']))
1080
1094
1081
1095
1082 @jsonrpc_method()
1096 @jsonrpc_method()
1083 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1097 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1084 """
1098 """
1085 Deletes a repository.
1099 Deletes a repository.
1086
1100
1087 * When the `forks` parameter is set it's possible to detach or delete
1101 * When the `forks` parameter is set it's possible to detach or delete
1088 forks of deleted repository.
1102 forks of deleted repository.
1089
1103
1090 This command can only be run using an |authtoken| with admin
1104 This command can only be run using an |authtoken| with admin
1091 permissions on the |repo|.
1105 permissions on the |repo|.
1092
1106
1093 :param apiuser: This is filled automatically from the |authtoken|.
1107 :param apiuser: This is filled automatically from the |authtoken|.
1094 :type apiuser: AuthUser
1108 :type apiuser: AuthUser
1095 :param repoid: Set the repository name or repository ID.
1109 :param repoid: Set the repository name or repository ID.
1096 :type repoid: str or int
1110 :type repoid: str or int
1097 :param forks: Set to `detach` or `delete` forks from the |repo|.
1111 :param forks: Set to `detach` or `delete` forks from the |repo|.
1098 :type forks: Optional(str)
1112 :type forks: Optional(str)
1099
1113
1100 Example error output:
1114 Example error output:
1101
1115
1102 .. code-block:: bash
1116 .. code-block:: bash
1103
1117
1104 id : <id_given_in_input>
1118 id : <id_given_in_input>
1105 result: {
1119 result: {
1106 "msg": "Deleted repository `<reponame>`",
1120 "msg": "Deleted repository `<reponame>`",
1107 "success": true
1121 "success": true
1108 }
1122 }
1109 error: null
1123 error: null
1110 """
1124 """
1111
1125
1112 repo = get_repo_or_error(repoid)
1126 repo = get_repo_or_error(repoid)
1113 if not has_superadmin_permission(apiuser):
1127 if not has_superadmin_permission(apiuser):
1114 _perms = ('repository.admin',)
1128 _perms = ('repository.admin',)
1115 validate_repo_permissions(apiuser, repoid, repo, _perms)
1129 validate_repo_permissions(apiuser, repoid, repo, _perms)
1116
1130
1117 try:
1131 try:
1118 handle_forks = Optional.extract(forks)
1132 handle_forks = Optional.extract(forks)
1119 _forks_msg = ''
1133 _forks_msg = ''
1120 _forks = [f for f in repo.forks]
1134 _forks = [f for f in repo.forks]
1121 if handle_forks == 'detach':
1135 if handle_forks == 'detach':
1122 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1136 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1123 elif handle_forks == 'delete':
1137 elif handle_forks == 'delete':
1124 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1138 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1125 elif _forks:
1139 elif _forks:
1126 raise JSONRPCError(
1140 raise JSONRPCError(
1127 'Cannot delete `%s` it still contains attached forks' %
1141 'Cannot delete `%s` it still contains attached forks' %
1128 (repo.repo_name,)
1142 (repo.repo_name,)
1129 )
1143 )
1130
1144
1131 RepoModel().delete(repo, forks=forks)
1145 RepoModel().delete(repo, forks=forks)
1132 Session().commit()
1146 Session().commit()
1133 return {
1147 return {
1134 'msg': 'Deleted repository `%s`%s' % (
1148 'msg': 'Deleted repository `%s`%s' % (
1135 repo.repo_name, _forks_msg),
1149 repo.repo_name, _forks_msg),
1136 'success': True
1150 'success': True
1137 }
1151 }
1138 except Exception:
1152 except Exception:
1139 log.exception("Exception occurred while trying to delete repo")
1153 log.exception("Exception occurred while trying to delete repo")
1140 raise JSONRPCError(
1154 raise JSONRPCError(
1141 'failed to delete repository `%s`' % (repo.repo_name,)
1155 'failed to delete repository `%s`' % (repo.repo_name,)
1142 )
1156 )
1143
1157
1144
1158
1145 #TODO: marcink, change name ?
1159 #TODO: marcink, change name ?
1146 @jsonrpc_method()
1160 @jsonrpc_method()
1147 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1161 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1148 """
1162 """
1149 Invalidates the cache for the specified repository.
1163 Invalidates the cache for the specified repository.
1150
1164
1151 This command can only be run using an |authtoken| with admin rights to
1165 This command can only be run using an |authtoken| with admin rights to
1152 the specified repository.
1166 the specified repository.
1153
1167
1154 This command takes the following options:
1168 This command takes the following options:
1155
1169
1156 :param apiuser: This is filled automatically from |authtoken|.
1170 :param apiuser: This is filled automatically from |authtoken|.
1157 :type apiuser: AuthUser
1171 :type apiuser: AuthUser
1158 :param repoid: Sets the repository name or repository ID.
1172 :param repoid: Sets the repository name or repository ID.
1159 :type repoid: str or int
1173 :type repoid: str or int
1160 :param delete_keys: This deletes the invalidated keys instead of
1174 :param delete_keys: This deletes the invalidated keys instead of
1161 just flagging them.
1175 just flagging them.
1162 :type delete_keys: Optional(``True`` | ``False``)
1176 :type delete_keys: Optional(``True`` | ``False``)
1163
1177
1164 Example output:
1178 Example output:
1165
1179
1166 .. code-block:: bash
1180 .. code-block:: bash
1167
1181
1168 id : <id_given_in_input>
1182 id : <id_given_in_input>
1169 result : {
1183 result : {
1170 'msg': Cache for repository `<repository name>` was invalidated,
1184 'msg': Cache for repository `<repository name>` was invalidated,
1171 'repository': <repository name>
1185 'repository': <repository name>
1172 }
1186 }
1173 error : null
1187 error : null
1174
1188
1175 Example error output:
1189 Example error output:
1176
1190
1177 .. code-block:: bash
1191 .. code-block:: bash
1178
1192
1179 id : <id_given_in_input>
1193 id : <id_given_in_input>
1180 result : null
1194 result : null
1181 error : {
1195 error : {
1182 'Error occurred during cache invalidation action'
1196 'Error occurred during cache invalidation action'
1183 }
1197 }
1184
1198
1185 """
1199 """
1186
1200
1187 repo = get_repo_or_error(repoid)
1201 repo = get_repo_or_error(repoid)
1188 if not has_superadmin_permission(apiuser):
1202 if not has_superadmin_permission(apiuser):
1189 _perms = ('repository.admin', 'repository.write',)
1203 _perms = ('repository.admin', 'repository.write',)
1190 validate_repo_permissions(apiuser, repoid, repo, _perms)
1204 validate_repo_permissions(apiuser, repoid, repo, _perms)
1191
1205
1192 delete = Optional.extract(delete_keys)
1206 delete = Optional.extract(delete_keys)
1193 try:
1207 try:
1194 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1208 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1195 return {
1209 return {
1196 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1210 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1197 'repository': repo.repo_name
1211 'repository': repo.repo_name
1198 }
1212 }
1199 except Exception:
1213 except Exception:
1200 log.exception(
1214 log.exception(
1201 "Exception occurred while trying to invalidate repo cache")
1215 "Exception occurred while trying to invalidate repo cache")
1202 raise JSONRPCError(
1216 raise JSONRPCError(
1203 'Error occurred during cache invalidation action'
1217 'Error occurred during cache invalidation action'
1204 )
1218 )
1205
1219
1206
1220
1207 #TODO: marcink, change name ?
1221 #TODO: marcink, change name ?
1208 @jsonrpc_method()
1222 @jsonrpc_method()
1209 def lock(request, apiuser, repoid, locked=Optional(None),
1223 def lock(request, apiuser, repoid, locked=Optional(None),
1210 userid=Optional(OAttr('apiuser'))):
1224 userid=Optional(OAttr('apiuser'))):
1211 """
1225 """
1212 Sets the lock state of the specified |repo| by the given user.
1226 Sets the lock state of the specified |repo| by the given user.
1213 From more information, see :ref:`repo-locking`.
1227 From more information, see :ref:`repo-locking`.
1214
1228
1215 * If the ``userid`` option is not set, the repository is locked to the
1229 * If the ``userid`` option is not set, the repository is locked to the
1216 user who called the method.
1230 user who called the method.
1217 * If the ``locked`` parameter is not set, the current lock state of the
1231 * If the ``locked`` parameter is not set, the current lock state of the
1218 repository is displayed.
1232 repository is displayed.
1219
1233
1220 This command can only be run using an |authtoken| with admin rights to
1234 This command can only be run using an |authtoken| with admin rights to
1221 the specified repository.
1235 the specified repository.
1222
1236
1223 This command takes the following options:
1237 This command takes the following options:
1224
1238
1225 :param apiuser: This is filled automatically from the |authtoken|.
1239 :param apiuser: This is filled automatically from the |authtoken|.
1226 :type apiuser: AuthUser
1240 :type apiuser: AuthUser
1227 :param repoid: Sets the repository name or repository ID.
1241 :param repoid: Sets the repository name or repository ID.
1228 :type repoid: str or int
1242 :type repoid: str or int
1229 :param locked: Sets the lock state.
1243 :param locked: Sets the lock state.
1230 :type locked: Optional(``True`` | ``False``)
1244 :type locked: Optional(``True`` | ``False``)
1231 :param userid: Set the repository lock to this user.
1245 :param userid: Set the repository lock to this user.
1232 :type userid: Optional(str or int)
1246 :type userid: Optional(str or int)
1233
1247
1234 Example error output:
1248 Example error output:
1235
1249
1236 .. code-block:: bash
1250 .. code-block:: bash
1237
1251
1238 id : <id_given_in_input>
1252 id : <id_given_in_input>
1239 result : {
1253 result : {
1240 'repo': '<reponame>',
1254 'repo': '<reponame>',
1241 'locked': <bool: lock state>,
1255 'locked': <bool: lock state>,
1242 'locked_since': <int: lock timestamp>,
1256 'locked_since': <int: lock timestamp>,
1243 'locked_by': <username of person who made the lock>,
1257 'locked_by': <username of person who made the lock>,
1244 'lock_reason': <str: reason for locking>,
1258 'lock_reason': <str: reason for locking>,
1245 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1259 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1246 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1260 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1247 or
1261 or
1248 'msg': 'Repo `<repository name>` not locked.'
1262 'msg': 'Repo `<repository name>` not locked.'
1249 or
1263 or
1250 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1264 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1251 }
1265 }
1252 error : null
1266 error : null
1253
1267
1254 Example error output:
1268 Example error output:
1255
1269
1256 .. code-block:: bash
1270 .. code-block:: bash
1257
1271
1258 id : <id_given_in_input>
1272 id : <id_given_in_input>
1259 result : null
1273 result : null
1260 error : {
1274 error : {
1261 'Error occurred locking repository `<reponame>`'
1275 'Error occurred locking repository `<reponame>`'
1262 }
1276 }
1263 """
1277 """
1264
1278
1265 repo = get_repo_or_error(repoid)
1279 repo = get_repo_or_error(repoid)
1266 if not has_superadmin_permission(apiuser):
1280 if not has_superadmin_permission(apiuser):
1267 # check if we have at least write permission for this repo !
1281 # check if we have at least write permission for this repo !
1268 _perms = ('repository.admin', 'repository.write',)
1282 _perms = ('repository.admin', 'repository.write',)
1269 validate_repo_permissions(apiuser, repoid, repo, _perms)
1283 validate_repo_permissions(apiuser, repoid, repo, _perms)
1270
1284
1271 # make sure normal user does not pass someone else userid,
1285 # make sure normal user does not pass someone else userid,
1272 # he is not allowed to do that
1286 # he is not allowed to do that
1273 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1287 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1274 raise JSONRPCError('userid is not the same as your user')
1288 raise JSONRPCError('userid is not the same as your user')
1275
1289
1276 if isinstance(userid, Optional):
1290 if isinstance(userid, Optional):
1277 userid = apiuser.user_id
1291 userid = apiuser.user_id
1278
1292
1279 user = get_user_or_error(userid)
1293 user = get_user_or_error(userid)
1280
1294
1281 if isinstance(locked, Optional):
1295 if isinstance(locked, Optional):
1282 lockobj = repo.locked
1296 lockobj = repo.locked
1283
1297
1284 if lockobj[0] is None:
1298 if lockobj[0] is None:
1285 _d = {
1299 _d = {
1286 'repo': repo.repo_name,
1300 'repo': repo.repo_name,
1287 'locked': False,
1301 'locked': False,
1288 'locked_since': None,
1302 'locked_since': None,
1289 'locked_by': None,
1303 'locked_by': None,
1290 'lock_reason': None,
1304 'lock_reason': None,
1291 'lock_state_changed': False,
1305 'lock_state_changed': False,
1292 'msg': 'Repo `%s` not locked.' % repo.repo_name
1306 'msg': 'Repo `%s` not locked.' % repo.repo_name
1293 }
1307 }
1294 return _d
1308 return _d
1295 else:
1309 else:
1296 _user_id, _time, _reason = lockobj
1310 _user_id, _time, _reason = lockobj
1297 lock_user = get_user_or_error(userid)
1311 lock_user = get_user_or_error(userid)
1298 _d = {
1312 _d = {
1299 'repo': repo.repo_name,
1313 'repo': repo.repo_name,
1300 'locked': True,
1314 'locked': True,
1301 'locked_since': _time,
1315 'locked_since': _time,
1302 'locked_by': lock_user.username,
1316 'locked_by': lock_user.username,
1303 'lock_reason': _reason,
1317 'lock_reason': _reason,
1304 'lock_state_changed': False,
1318 'lock_state_changed': False,
1305 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1319 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1306 % (repo.repo_name, lock_user.username,
1320 % (repo.repo_name, lock_user.username,
1307 json.dumps(time_to_datetime(_time))))
1321 json.dumps(time_to_datetime(_time))))
1308 }
1322 }
1309 return _d
1323 return _d
1310
1324
1311 # force locked state through a flag
1325 # force locked state through a flag
1312 else:
1326 else:
1313 locked = str2bool(locked)
1327 locked = str2bool(locked)
1314 lock_reason = Repository.LOCK_API
1328 lock_reason = Repository.LOCK_API
1315 try:
1329 try:
1316 if locked:
1330 if locked:
1317 lock_time = time.time()
1331 lock_time = time.time()
1318 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1332 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1319 else:
1333 else:
1320 lock_time = None
1334 lock_time = None
1321 Repository.unlock(repo)
1335 Repository.unlock(repo)
1322 _d = {
1336 _d = {
1323 'repo': repo.repo_name,
1337 'repo': repo.repo_name,
1324 'locked': locked,
1338 'locked': locked,
1325 'locked_since': lock_time,
1339 'locked_since': lock_time,
1326 'locked_by': user.username,
1340 'locked_by': user.username,
1327 'lock_reason': lock_reason,
1341 'lock_reason': lock_reason,
1328 'lock_state_changed': True,
1342 'lock_state_changed': True,
1329 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1343 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1330 % (user.username, repo.repo_name, locked))
1344 % (user.username, repo.repo_name, locked))
1331 }
1345 }
1332 return _d
1346 return _d
1333 except Exception:
1347 except Exception:
1334 log.exception(
1348 log.exception(
1335 "Exception occurred while trying to lock repository")
1349 "Exception occurred while trying to lock repository")
1336 raise JSONRPCError(
1350 raise JSONRPCError(
1337 'Error occurred locking repository `%s`' % repo.repo_name
1351 'Error occurred locking repository `%s`' % repo.repo_name
1338 )
1352 )
1339
1353
1340
1354
1341 @jsonrpc_method()
1355 @jsonrpc_method()
1342 def comment_commit(
1356 def comment_commit(
1343 request, apiuser, repoid, commit_id, message,
1357 request, apiuser, repoid, commit_id, message,
1344 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1358 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1345 """
1359 """
1346 Set a commit comment, and optionally change the status of the commit.
1360 Set a commit comment, and optionally change the status of the commit.
1347
1361
1348 :param apiuser: This is filled automatically from the |authtoken|.
1362 :param apiuser: This is filled automatically from the |authtoken|.
1349 :type apiuser: AuthUser
1363 :type apiuser: AuthUser
1350 :param repoid: Set the repository name or repository ID.
1364 :param repoid: Set the repository name or repository ID.
1351 :type repoid: str or int
1365 :type repoid: str or int
1352 :param commit_id: Specify the commit_id for which to set a comment.
1366 :param commit_id: Specify the commit_id for which to set a comment.
1353 :type commit_id: str
1367 :type commit_id: str
1354 :param message: The comment text.
1368 :param message: The comment text.
1355 :type message: str
1369 :type message: str
1356 :param userid: Set the user name of the comment creator.
1370 :param userid: Set the user name of the comment creator.
1357 :type userid: Optional(str or int)
1371 :type userid: Optional(str or int)
1358 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1372 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1359 'under_review'
1373 'under_review'
1360 :type status: str
1374 :type status: str
1361
1375
1362 Example error output:
1376 Example error output:
1363
1377
1364 .. code-block:: json
1378 .. code-block:: json
1365
1379
1366 {
1380 {
1367 "id" : <id_given_in_input>,
1381 "id" : <id_given_in_input>,
1368 "result" : {
1382 "result" : {
1369 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1383 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1370 "status_change": null or <status>,
1384 "status_change": null or <status>,
1371 "success": true
1385 "success": true
1372 },
1386 },
1373 "error" : null
1387 "error" : null
1374 }
1388 }
1375
1389
1376 """
1390 """
1377 repo = get_repo_or_error(repoid)
1391 repo = get_repo_or_error(repoid)
1378 if not has_superadmin_permission(apiuser):
1392 if not has_superadmin_permission(apiuser):
1379 _perms = ('repository.read', 'repository.write', 'repository.admin')
1393 _perms = ('repository.read', 'repository.write', 'repository.admin')
1380 validate_repo_permissions(apiuser, repoid, repo, _perms)
1394 validate_repo_permissions(apiuser, repoid, repo, _perms)
1381
1395
1382 if isinstance(userid, Optional):
1396 if isinstance(userid, Optional):
1383 userid = apiuser.user_id
1397 userid = apiuser.user_id
1384
1398
1385 user = get_user_or_error(userid)
1399 user = get_user_or_error(userid)
1386 status = Optional.extract(status)
1400 status = Optional.extract(status)
1387
1401
1388 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1402 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1389 if status and status not in allowed_statuses:
1403 if status and status not in allowed_statuses:
1390 raise JSONRPCError('Bad status, must be on '
1404 raise JSONRPCError('Bad status, must be on '
1391 'of %s got %s' % (allowed_statuses, status,))
1405 'of %s got %s' % (allowed_statuses, status,))
1392
1406
1393 try:
1407 try:
1394 rc_config = SettingsModel().get_all_settings()
1408 rc_config = SettingsModel().get_all_settings()
1395 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1409 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1396 status_change_label = ChangesetStatus.get_status_lbl(status)
1410 status_change_label = ChangesetStatus.get_status_lbl(status)
1397 comm = ChangesetCommentsModel().create(
1411 comm = ChangesetCommentsModel().create(
1398 message, repo, user, revision=commit_id,
1412 message, repo, user, revision=commit_id,
1399 status_change=status_change_label,
1413 status_change=status_change_label,
1400 status_change_type=status,
1414 status_change_type=status,
1401 renderer=renderer)
1415 renderer=renderer)
1402 if status:
1416 if status:
1403 # also do a status change
1417 # also do a status change
1404 try:
1418 try:
1405 ChangesetStatusModel().set_status(
1419 ChangesetStatusModel().set_status(
1406 repo, status, user, comm, revision=commit_id,
1420 repo, status, user, comm, revision=commit_id,
1407 dont_allow_on_closed_pull_request=True
1421 dont_allow_on_closed_pull_request=True
1408 )
1422 )
1409 except StatusChangeOnClosedPullRequestError:
1423 except StatusChangeOnClosedPullRequestError:
1410 log.exception(
1424 log.exception(
1411 "Exception occurred while trying to change repo commit status")
1425 "Exception occurred while trying to change repo commit status")
1412 msg = ('Changing status on a changeset associated with '
1426 msg = ('Changing status on a changeset associated with '
1413 'a closed pull request is not allowed')
1427 'a closed pull request is not allowed')
1414 raise JSONRPCError(msg)
1428 raise JSONRPCError(msg)
1415
1429
1416 Session().commit()
1430 Session().commit()
1417 return {
1431 return {
1418 'msg': (
1432 'msg': (
1419 'Commented on commit `%s` for repository `%s`' % (
1433 'Commented on commit `%s` for repository `%s`' % (
1420 comm.revision, repo.repo_name)),
1434 comm.revision, repo.repo_name)),
1421 'status_change': status,
1435 'status_change': status,
1422 'success': True,
1436 'success': True,
1423 }
1437 }
1424 except JSONRPCError:
1438 except JSONRPCError:
1425 # catch any inside errors, and re-raise them to prevent from
1439 # catch any inside errors, and re-raise them to prevent from
1426 # below global catch to silence them
1440 # below global catch to silence them
1427 raise
1441 raise
1428 except Exception:
1442 except Exception:
1429 log.exception("Exception occurred while trying to comment on commit")
1443 log.exception("Exception occurred while trying to comment on commit")
1430 raise JSONRPCError(
1444 raise JSONRPCError(
1431 'failed to set comment on repository `%s`' % (repo.repo_name,)
1445 'failed to set comment on repository `%s`' % (repo.repo_name,)
1432 )
1446 )
1433
1447
1434
1448
1435 @jsonrpc_method()
1449 @jsonrpc_method()
1436 def grant_user_permission(request, apiuser, repoid, userid, perm):
1450 def grant_user_permission(request, apiuser, repoid, userid, perm):
1437 """
1451 """
1438 Grant permissions for the specified user on the given repository,
1452 Grant permissions for the specified user on the given repository,
1439 or update existing permissions if found.
1453 or update existing permissions if found.
1440
1454
1441 This command can only be run using an |authtoken| with admin
1455 This command can only be run using an |authtoken| with admin
1442 permissions on the |repo|.
1456 permissions on the |repo|.
1443
1457
1444 :param apiuser: This is filled automatically from the |authtoken|.
1458 :param apiuser: This is filled automatically from the |authtoken|.
1445 :type apiuser: AuthUser
1459 :type apiuser: AuthUser
1446 :param repoid: Set the repository name or repository ID.
1460 :param repoid: Set the repository name or repository ID.
1447 :type repoid: str or int
1461 :type repoid: str or int
1448 :param userid: Set the user name.
1462 :param userid: Set the user name.
1449 :type userid: str
1463 :type userid: str
1450 :param perm: Set the user permissions, using the following format
1464 :param perm: Set the user permissions, using the following format
1451 ``(repository.(none|read|write|admin))``
1465 ``(repository.(none|read|write|admin))``
1452 :type perm: str
1466 :type perm: str
1453
1467
1454 Example output:
1468 Example output:
1455
1469
1456 .. code-block:: bash
1470 .. code-block:: bash
1457
1471
1458 id : <id_given_in_input>
1472 id : <id_given_in_input>
1459 result: {
1473 result: {
1460 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1474 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1461 "success": true
1475 "success": true
1462 }
1476 }
1463 error: null
1477 error: null
1464 """
1478 """
1465
1479
1466 repo = get_repo_or_error(repoid)
1480 repo = get_repo_or_error(repoid)
1467 user = get_user_or_error(userid)
1481 user = get_user_or_error(userid)
1468 perm = get_perm_or_error(perm)
1482 perm = get_perm_or_error(perm)
1469 if not has_superadmin_permission(apiuser):
1483 if not has_superadmin_permission(apiuser):
1470 _perms = ('repository.admin',)
1484 _perms = ('repository.admin',)
1471 validate_repo_permissions(apiuser, repoid, repo, _perms)
1485 validate_repo_permissions(apiuser, repoid, repo, _perms)
1472
1486
1473 try:
1487 try:
1474
1488
1475 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1489 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1476
1490
1477 Session().commit()
1491 Session().commit()
1478 return {
1492 return {
1479 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1493 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1480 perm.permission_name, user.username, repo.repo_name
1494 perm.permission_name, user.username, repo.repo_name
1481 ),
1495 ),
1482 'success': True
1496 'success': True
1483 }
1497 }
1484 except Exception:
1498 except Exception:
1485 log.exception(
1499 log.exception(
1486 "Exception occurred while trying edit permissions for repo")
1500 "Exception occurred while trying edit permissions for repo")
1487 raise JSONRPCError(
1501 raise JSONRPCError(
1488 'failed to edit permission for user: `%s` in repo: `%s`' % (
1502 'failed to edit permission for user: `%s` in repo: `%s`' % (
1489 userid, repoid
1503 userid, repoid
1490 )
1504 )
1491 )
1505 )
1492
1506
1493
1507
1494 @jsonrpc_method()
1508 @jsonrpc_method()
1495 def revoke_user_permission(request, apiuser, repoid, userid):
1509 def revoke_user_permission(request, apiuser, repoid, userid):
1496 """
1510 """
1497 Revoke permission for a user on the specified repository.
1511 Revoke permission for a user on the specified repository.
1498
1512
1499 This command can only be run using an |authtoken| with admin
1513 This command can only be run using an |authtoken| with admin
1500 permissions on the |repo|.
1514 permissions on the |repo|.
1501
1515
1502 :param apiuser: This is filled automatically from the |authtoken|.
1516 :param apiuser: This is filled automatically from the |authtoken|.
1503 :type apiuser: AuthUser
1517 :type apiuser: AuthUser
1504 :param repoid: Set the repository name or repository ID.
1518 :param repoid: Set the repository name or repository ID.
1505 :type repoid: str or int
1519 :type repoid: str or int
1506 :param userid: Set the user name of revoked user.
1520 :param userid: Set the user name of revoked user.
1507 :type userid: str or int
1521 :type userid: str or int
1508
1522
1509 Example error output:
1523 Example error output:
1510
1524
1511 .. code-block:: bash
1525 .. code-block:: bash
1512
1526
1513 id : <id_given_in_input>
1527 id : <id_given_in_input>
1514 result: {
1528 result: {
1515 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1529 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1516 "success": true
1530 "success": true
1517 }
1531 }
1518 error: null
1532 error: null
1519 """
1533 """
1520
1534
1521 repo = get_repo_or_error(repoid)
1535 repo = get_repo_or_error(repoid)
1522 user = get_user_or_error(userid)
1536 user = get_user_or_error(userid)
1523 if not has_superadmin_permission(apiuser):
1537 if not has_superadmin_permission(apiuser):
1524 _perms = ('repository.admin',)
1538 _perms = ('repository.admin',)
1525 validate_repo_permissions(apiuser, repoid, repo, _perms)
1539 validate_repo_permissions(apiuser, repoid, repo, _perms)
1526
1540
1527 try:
1541 try:
1528 RepoModel().revoke_user_permission(repo=repo, user=user)
1542 RepoModel().revoke_user_permission(repo=repo, user=user)
1529 Session().commit()
1543 Session().commit()
1530 return {
1544 return {
1531 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1545 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1532 user.username, repo.repo_name
1546 user.username, repo.repo_name
1533 ),
1547 ),
1534 'success': True
1548 'success': True
1535 }
1549 }
1536 except Exception:
1550 except Exception:
1537 log.exception(
1551 log.exception(
1538 "Exception occurred while trying revoke permissions to repo")
1552 "Exception occurred while trying revoke permissions to repo")
1539 raise JSONRPCError(
1553 raise JSONRPCError(
1540 'failed to edit permission for user: `%s` in repo: `%s`' % (
1554 'failed to edit permission for user: `%s` in repo: `%s`' % (
1541 userid, repoid
1555 userid, repoid
1542 )
1556 )
1543 )
1557 )
1544
1558
1545
1559
1546 @jsonrpc_method()
1560 @jsonrpc_method()
1547 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1561 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1548 """
1562 """
1549 Grant permission for a user group on the specified repository,
1563 Grant permission for a user group on the specified repository,
1550 or update existing permissions.
1564 or update existing permissions.
1551
1565
1552 This command can only be run using an |authtoken| with admin
1566 This command can only be run using an |authtoken| with admin
1553 permissions on the |repo|.
1567 permissions on the |repo|.
1554
1568
1555 :param apiuser: This is filled automatically from the |authtoken|.
1569 :param apiuser: This is filled automatically from the |authtoken|.
1556 :type apiuser: AuthUser
1570 :type apiuser: AuthUser
1557 :param repoid: Set the repository name or repository ID.
1571 :param repoid: Set the repository name or repository ID.
1558 :type repoid: str or int
1572 :type repoid: str or int
1559 :param usergroupid: Specify the ID of the user group.
1573 :param usergroupid: Specify the ID of the user group.
1560 :type usergroupid: str or int
1574 :type usergroupid: str or int
1561 :param perm: Set the user group permissions using the following
1575 :param perm: Set the user group permissions using the following
1562 format: (repository.(none|read|write|admin))
1576 format: (repository.(none|read|write|admin))
1563 :type perm: str
1577 :type perm: str
1564
1578
1565 Example output:
1579 Example output:
1566
1580
1567 .. code-block:: bash
1581 .. code-block:: bash
1568
1582
1569 id : <id_given_in_input>
1583 id : <id_given_in_input>
1570 result : {
1584 result : {
1571 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1585 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1572 "success": true
1586 "success": true
1573
1587
1574 }
1588 }
1575 error : null
1589 error : null
1576
1590
1577 Example error output:
1591 Example error output:
1578
1592
1579 .. code-block:: bash
1593 .. code-block:: bash
1580
1594
1581 id : <id_given_in_input>
1595 id : <id_given_in_input>
1582 result : null
1596 result : null
1583 error : {
1597 error : {
1584 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1598 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1585 }
1599 }
1586
1600
1587 """
1601 """
1588
1602
1589 repo = get_repo_or_error(repoid)
1603 repo = get_repo_or_error(repoid)
1590 perm = get_perm_or_error(perm)
1604 perm = get_perm_or_error(perm)
1591 if not has_superadmin_permission(apiuser):
1605 if not has_superadmin_permission(apiuser):
1592 _perms = ('repository.admin',)
1606 _perms = ('repository.admin',)
1593 validate_repo_permissions(apiuser, repoid, repo, _perms)
1607 validate_repo_permissions(apiuser, repoid, repo, _perms)
1594
1608
1595 user_group = get_user_group_or_error(usergroupid)
1609 user_group = get_user_group_or_error(usergroupid)
1596 if not has_superadmin_permission(apiuser):
1610 if not has_superadmin_permission(apiuser):
1597 # check if we have at least read permission for this user group !
1611 # check if we have at least read permission for this user group !
1598 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1612 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1599 if not HasUserGroupPermissionAnyApi(*_perms)(
1613 if not HasUserGroupPermissionAnyApi(*_perms)(
1600 user=apiuser, user_group_name=user_group.users_group_name):
1614 user=apiuser, user_group_name=user_group.users_group_name):
1601 raise JSONRPCError(
1615 raise JSONRPCError(
1602 'user group `%s` does not exist' % (usergroupid,))
1616 'user group `%s` does not exist' % (usergroupid,))
1603
1617
1604 try:
1618 try:
1605 RepoModel().grant_user_group_permission(
1619 RepoModel().grant_user_group_permission(
1606 repo=repo, group_name=user_group, perm=perm)
1620 repo=repo, group_name=user_group, perm=perm)
1607
1621
1608 Session().commit()
1622 Session().commit()
1609 return {
1623 return {
1610 'msg': 'Granted perm: `%s` for user group: `%s` in '
1624 'msg': 'Granted perm: `%s` for user group: `%s` in '
1611 'repo: `%s`' % (
1625 'repo: `%s`' % (
1612 perm.permission_name, user_group.users_group_name,
1626 perm.permission_name, user_group.users_group_name,
1613 repo.repo_name
1627 repo.repo_name
1614 ),
1628 ),
1615 'success': True
1629 'success': True
1616 }
1630 }
1617 except Exception:
1631 except Exception:
1618 log.exception(
1632 log.exception(
1619 "Exception occurred while trying change permission on repo")
1633 "Exception occurred while trying change permission on repo")
1620 raise JSONRPCError(
1634 raise JSONRPCError(
1621 'failed to edit permission for user group: `%s` in '
1635 'failed to edit permission for user group: `%s` in '
1622 'repo: `%s`' % (
1636 'repo: `%s`' % (
1623 usergroupid, repo.repo_name
1637 usergroupid, repo.repo_name
1624 )
1638 )
1625 )
1639 )
1626
1640
1627
1641
1628 @jsonrpc_method()
1642 @jsonrpc_method()
1629 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1643 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1630 """
1644 """
1631 Revoke the permissions of a user group on a given repository.
1645 Revoke the permissions of a user group on a given repository.
1632
1646
1633 This command can only be run using an |authtoken| with admin
1647 This command can only be run using an |authtoken| with admin
1634 permissions on the |repo|.
1648 permissions on the |repo|.
1635
1649
1636 :param apiuser: This is filled automatically from the |authtoken|.
1650 :param apiuser: This is filled automatically from the |authtoken|.
1637 :type apiuser: AuthUser
1651 :type apiuser: AuthUser
1638 :param repoid: Set the repository name or repository ID.
1652 :param repoid: Set the repository name or repository ID.
1639 :type repoid: str or int
1653 :type repoid: str or int
1640 :param usergroupid: Specify the user group ID.
1654 :param usergroupid: Specify the user group ID.
1641 :type usergroupid: str or int
1655 :type usergroupid: str or int
1642
1656
1643 Example output:
1657 Example output:
1644
1658
1645 .. code-block:: bash
1659 .. code-block:: bash
1646
1660
1647 id : <id_given_in_input>
1661 id : <id_given_in_input>
1648 result: {
1662 result: {
1649 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1663 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1650 "success": true
1664 "success": true
1651 }
1665 }
1652 error: null
1666 error: null
1653 """
1667 """
1654
1668
1655 repo = get_repo_or_error(repoid)
1669 repo = get_repo_or_error(repoid)
1656 if not has_superadmin_permission(apiuser):
1670 if not has_superadmin_permission(apiuser):
1657 _perms = ('repository.admin',)
1671 _perms = ('repository.admin',)
1658 validate_repo_permissions(apiuser, repoid, repo, _perms)
1672 validate_repo_permissions(apiuser, repoid, repo, _perms)
1659
1673
1660 user_group = get_user_group_or_error(usergroupid)
1674 user_group = get_user_group_or_error(usergroupid)
1661 if not has_superadmin_permission(apiuser):
1675 if not has_superadmin_permission(apiuser):
1662 # check if we have at least read permission for this user group !
1676 # check if we have at least read permission for this user group !
1663 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1677 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1664 if not HasUserGroupPermissionAnyApi(*_perms)(
1678 if not HasUserGroupPermissionAnyApi(*_perms)(
1665 user=apiuser, user_group_name=user_group.users_group_name):
1679 user=apiuser, user_group_name=user_group.users_group_name):
1666 raise JSONRPCError(
1680 raise JSONRPCError(
1667 'user group `%s` does not exist' % (usergroupid,))
1681 'user group `%s` does not exist' % (usergroupid,))
1668
1682
1669 try:
1683 try:
1670 RepoModel().revoke_user_group_permission(
1684 RepoModel().revoke_user_group_permission(
1671 repo=repo, group_name=user_group)
1685 repo=repo, group_name=user_group)
1672
1686
1673 Session().commit()
1687 Session().commit()
1674 return {
1688 return {
1675 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1689 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1676 user_group.users_group_name, repo.repo_name
1690 user_group.users_group_name, repo.repo_name
1677 ),
1691 ),
1678 'success': True
1692 'success': True
1679 }
1693 }
1680 except Exception:
1694 except Exception:
1681 log.exception("Exception occurred while trying revoke "
1695 log.exception("Exception occurred while trying revoke "
1682 "user group permission on repo")
1696 "user group permission on repo")
1683 raise JSONRPCError(
1697 raise JSONRPCError(
1684 'failed to edit permission for user group: `%s` in '
1698 'failed to edit permission for user group: `%s` in '
1685 'repo: `%s`' % (
1699 'repo: `%s`' % (
1686 user_group.users_group_name, repo.repo_name
1700 user_group.users_group_name, repo.repo_name
1687 )
1701 )
1688 )
1702 )
1689
1703
1690
1704
1691 @jsonrpc_method()
1705 @jsonrpc_method()
1692 def pull(request, apiuser, repoid):
1706 def pull(request, apiuser, repoid):
1693 """
1707 """
1694 Triggers a pull on the given repository from a remote location. You
1708 Triggers a pull on the given repository from a remote location. You
1695 can use this to keep remote repositories up-to-date.
1709 can use this to keep remote repositories up-to-date.
1696
1710
1697 This command can only be run using an |authtoken| with admin
1711 This command can only be run using an |authtoken| with admin
1698 rights to the specified repository. For more information,
1712 rights to the specified repository. For more information,
1699 see :ref:`config-token-ref`.
1713 see :ref:`config-token-ref`.
1700
1714
1701 This command takes the following options:
1715 This command takes the following options:
1702
1716
1703 :param apiuser: This is filled automatically from the |authtoken|.
1717 :param apiuser: This is filled automatically from the |authtoken|.
1704 :type apiuser: AuthUser
1718 :type apiuser: AuthUser
1705 :param repoid: The repository name or repository ID.
1719 :param repoid: The repository name or repository ID.
1706 :type repoid: str or int
1720 :type repoid: str or int
1707
1721
1708 Example output:
1722 Example output:
1709
1723
1710 .. code-block:: bash
1724 .. code-block:: bash
1711
1725
1712 id : <id_given_in_input>
1726 id : <id_given_in_input>
1713 result : {
1727 result : {
1714 "msg": "Pulled from `<repository name>`"
1728 "msg": "Pulled from `<repository name>`"
1715 "repository": "<repository name>"
1729 "repository": "<repository name>"
1716 }
1730 }
1717 error : null
1731 error : null
1718
1732
1719 Example error output:
1733 Example error output:
1720
1734
1721 .. code-block:: bash
1735 .. code-block:: bash
1722
1736
1723 id : <id_given_in_input>
1737 id : <id_given_in_input>
1724 result : null
1738 result : null
1725 error : {
1739 error : {
1726 "Unable to pull changes from `<reponame>`"
1740 "Unable to pull changes from `<reponame>`"
1727 }
1741 }
1728
1742
1729 """
1743 """
1730
1744
1731 repo = get_repo_or_error(repoid)
1745 repo = get_repo_or_error(repoid)
1732 if not has_superadmin_permission(apiuser):
1746 if not has_superadmin_permission(apiuser):
1733 _perms = ('repository.admin',)
1747 _perms = ('repository.admin',)
1734 validate_repo_permissions(apiuser, repoid, repo, _perms)
1748 validate_repo_permissions(apiuser, repoid, repo, _perms)
1735
1749
1736 try:
1750 try:
1737 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1751 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1738 return {
1752 return {
1739 'msg': 'Pulled from `%s`' % repo.repo_name,
1753 'msg': 'Pulled from `%s`' % repo.repo_name,
1740 'repository': repo.repo_name
1754 'repository': repo.repo_name
1741 }
1755 }
1742 except Exception:
1756 except Exception:
1743 log.exception("Exception occurred while trying to "
1757 log.exception("Exception occurred while trying to "
1744 "pull changes from remote location")
1758 "pull changes from remote location")
1745 raise JSONRPCError(
1759 raise JSONRPCError(
1746 'Unable to pull changes from `%s`' % repo.repo_name
1760 'Unable to pull changes from `%s`' % repo.repo_name
1747 )
1761 )
1748
1762
1749
1763
1750 @jsonrpc_method()
1764 @jsonrpc_method()
1751 def strip(request, apiuser, repoid, revision, branch):
1765 def strip(request, apiuser, repoid, revision, branch):
1752 """
1766 """
1753 Strips the given revision from the specified repository.
1767 Strips the given revision from the specified repository.
1754
1768
1755 * This will remove the revision and all of its decendants.
1769 * This will remove the revision and all of its decendants.
1756
1770
1757 This command can only be run using an |authtoken| with admin rights to
1771 This command can only be run using an |authtoken| with admin rights to
1758 the specified repository.
1772 the specified repository.
1759
1773
1760 This command takes the following options:
1774 This command takes the following options:
1761
1775
1762 :param apiuser: This is filled automatically from the |authtoken|.
1776 :param apiuser: This is filled automatically from the |authtoken|.
1763 :type apiuser: AuthUser
1777 :type apiuser: AuthUser
1764 :param repoid: The repository name or repository ID.
1778 :param repoid: The repository name or repository ID.
1765 :type repoid: str or int
1779 :type repoid: str or int
1766 :param revision: The revision you wish to strip.
1780 :param revision: The revision you wish to strip.
1767 :type revision: str
1781 :type revision: str
1768 :param branch: The branch from which to strip the revision.
1782 :param branch: The branch from which to strip the revision.
1769 :type branch: str
1783 :type branch: str
1770
1784
1771 Example output:
1785 Example output:
1772
1786
1773 .. code-block:: bash
1787 .. code-block:: bash
1774
1788
1775 id : <id_given_in_input>
1789 id : <id_given_in_input>
1776 result : {
1790 result : {
1777 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1791 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1778 "repository": "<repository name>"
1792 "repository": "<repository name>"
1779 }
1793 }
1780 error : null
1794 error : null
1781
1795
1782 Example error output:
1796 Example error output:
1783
1797
1784 .. code-block:: bash
1798 .. code-block:: bash
1785
1799
1786 id : <id_given_in_input>
1800 id : <id_given_in_input>
1787 result : null
1801 result : null
1788 error : {
1802 error : {
1789 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1803 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1790 }
1804 }
1791
1805
1792 """
1806 """
1793
1807
1794 repo = get_repo_or_error(repoid)
1808 repo = get_repo_or_error(repoid)
1795 if not has_superadmin_permission(apiuser):
1809 if not has_superadmin_permission(apiuser):
1796 _perms = ('repository.admin',)
1810 _perms = ('repository.admin',)
1797 validate_repo_permissions(apiuser, repoid, repo, _perms)
1811 validate_repo_permissions(apiuser, repoid, repo, _perms)
1798
1812
1799 try:
1813 try:
1800 ScmModel().strip(repo, revision, branch)
1814 ScmModel().strip(repo, revision, branch)
1801 return {
1815 return {
1802 'msg': 'Stripped commit %s from repo `%s`' % (
1816 'msg': 'Stripped commit %s from repo `%s`' % (
1803 revision, repo.repo_name),
1817 revision, repo.repo_name),
1804 'repository': repo.repo_name
1818 'repository': repo.repo_name
1805 }
1819 }
1806 except Exception:
1820 except Exception:
1807 log.exception("Exception while trying to strip")
1821 log.exception("Exception while trying to strip")
1808 raise JSONRPCError(
1822 raise JSONRPCError(
1809 'Unable to strip commit %s from repo `%s`' % (
1823 'Unable to strip commit %s from repo `%s`' % (
1810 revision, repo.repo_name)
1824 revision, repo.repo_name)
1811 )
1825 )
1812
1826
1813
1827
1814 @jsonrpc_method()
1828 @jsonrpc_method()
1815 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1829 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1816 """
1830 """
1817 Returns all settings for a repository. If key is given it only returns the
1831 Returns all settings for a repository. If key is given it only returns the
1818 setting identified by the key or null.
1832 setting identified by the key or null.
1819
1833
1820 :param apiuser: This is filled automatically from the |authtoken|.
1834 :param apiuser: This is filled automatically from the |authtoken|.
1821 :type apiuser: AuthUser
1835 :type apiuser: AuthUser
1822 :param repoid: The repository name or repository id.
1836 :param repoid: The repository name or repository id.
1823 :type repoid: str or int
1837 :type repoid: str or int
1824 :param key: Key of the setting to return.
1838 :param key: Key of the setting to return.
1825 :type: key: Optional(str)
1839 :type: key: Optional(str)
1826
1840
1827 Example output:
1841 Example output:
1828
1842
1829 .. code-block:: bash
1843 .. code-block:: bash
1830
1844
1831 {
1845 {
1832 "error": null,
1846 "error": null,
1833 "id": 237,
1847 "id": 237,
1834 "result": {
1848 "result": {
1835 "extensions_largefiles": true,
1849 "extensions_largefiles": true,
1836 "hooks_changegroup_push_logger": true,
1850 "hooks_changegroup_push_logger": true,
1837 "hooks_changegroup_repo_size": false,
1851 "hooks_changegroup_repo_size": false,
1838 "hooks_outgoing_pull_logger": true,
1852 "hooks_outgoing_pull_logger": true,
1839 "phases_publish": "True",
1853 "phases_publish": "True",
1840 "rhodecode_hg_use_rebase_for_merging": true,
1854 "rhodecode_hg_use_rebase_for_merging": true,
1841 "rhodecode_pr_merge_enabled": true,
1855 "rhodecode_pr_merge_enabled": true,
1842 "rhodecode_use_outdated_comments": true
1856 "rhodecode_use_outdated_comments": true
1843 }
1857 }
1844 }
1858 }
1845 """
1859 """
1846
1860
1847 # Restrict access to this api method to admins only.
1861 # Restrict access to this api method to admins only.
1848 if not has_superadmin_permission(apiuser):
1862 if not has_superadmin_permission(apiuser):
1849 raise JSONRPCForbidden()
1863 raise JSONRPCForbidden()
1850
1864
1851 try:
1865 try:
1852 repo = get_repo_or_error(repoid)
1866 repo = get_repo_or_error(repoid)
1853 settings_model = VcsSettingsModel(repo=repo)
1867 settings_model = VcsSettingsModel(repo=repo)
1854 settings = settings_model.get_global_settings()
1868 settings = settings_model.get_global_settings()
1855 settings.update(settings_model.get_repo_settings())
1869 settings.update(settings_model.get_repo_settings())
1856
1870
1857 # If only a single setting is requested fetch it from all settings.
1871 # If only a single setting is requested fetch it from all settings.
1858 key = Optional.extract(key)
1872 key = Optional.extract(key)
1859 if key is not None:
1873 if key is not None:
1860 settings = settings.get(key, None)
1874 settings = settings.get(key, None)
1861 except Exception:
1875 except Exception:
1862 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1876 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1863 log.exception(msg)
1877 log.exception(msg)
1864 raise JSONRPCError(msg)
1878 raise JSONRPCError(msg)
1865
1879
1866 return settings
1880 return settings
1867
1881
1868
1882
1869 @jsonrpc_method()
1883 @jsonrpc_method()
1870 def set_repo_settings(request, apiuser, repoid, settings):
1884 def set_repo_settings(request, apiuser, repoid, settings):
1871 """
1885 """
1872 Update repository settings. Returns true on success.
1886 Update repository settings. Returns true on success.
1873
1887
1874 :param apiuser: This is filled automatically from the |authtoken|.
1888 :param apiuser: This is filled automatically from the |authtoken|.
1875 :type apiuser: AuthUser
1889 :type apiuser: AuthUser
1876 :param repoid: The repository name or repository id.
1890 :param repoid: The repository name or repository id.
1877 :type repoid: str or int
1891 :type repoid: str or int
1878 :param settings: The new settings for the repository.
1892 :param settings: The new settings for the repository.
1879 :type: settings: dict
1893 :type: settings: dict
1880
1894
1881 Example output:
1895 Example output:
1882
1896
1883 .. code-block:: bash
1897 .. code-block:: bash
1884
1898
1885 {
1899 {
1886 "error": null,
1900 "error": null,
1887 "id": 237,
1901 "id": 237,
1888 "result": true
1902 "result": true
1889 }
1903 }
1890 """
1904 """
1891 # Restrict access to this api method to admins only.
1905 # Restrict access to this api method to admins only.
1892 if not has_superadmin_permission(apiuser):
1906 if not has_superadmin_permission(apiuser):
1893 raise JSONRPCForbidden()
1907 raise JSONRPCForbidden()
1894
1908
1895 if type(settings) is not dict:
1909 if type(settings) is not dict:
1896 raise JSONRPCError('Settings have to be a JSON Object.')
1910 raise JSONRPCError('Settings have to be a JSON Object.')
1897
1911
1898 try:
1912 try:
1899 settings_model = VcsSettingsModel(repo=repoid)
1913 settings_model = VcsSettingsModel(repo=repoid)
1900
1914
1901 # Merge global, repo and incoming settings.
1915 # Merge global, repo and incoming settings.
1902 new_settings = settings_model.get_global_settings()
1916 new_settings = settings_model.get_global_settings()
1903 new_settings.update(settings_model.get_repo_settings())
1917 new_settings.update(settings_model.get_repo_settings())
1904 new_settings.update(settings)
1918 new_settings.update(settings)
1905
1919
1906 # Update the settings.
1920 # Update the settings.
1907 inherit_global_settings = new_settings.get(
1921 inherit_global_settings = new_settings.get(
1908 'inherit_global_settings', False)
1922 'inherit_global_settings', False)
1909 settings_model.create_or_update_repo_settings(
1923 settings_model.create_or_update_repo_settings(
1910 new_settings, inherit_global_settings=inherit_global_settings)
1924 new_settings, inherit_global_settings=inherit_global_settings)
1911 Session().commit()
1925 Session().commit()
1912 except Exception:
1926 except Exception:
1913 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1927 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1914 log.exception(msg)
1928 log.exception(msg)
1915 raise JSONRPCError(msg)
1929 raise JSONRPCError(msg)
1916
1930
1917 # Indicate success.
1931 # Indicate success.
1918 return True
1932 return True
@@ -1,1555 +1,1567 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24
24
25 import collections
25 import collections
26 import datetime
26 import datetime
27 import itertools
27 import itertools
28 import logging
28 import logging
29 import os
29 import os
30 import time
30 import time
31 import warnings
31 import warnings
32
32
33 from zope.cachedescriptors.property import Lazy as LazyProperty
33 from zope.cachedescriptors.property import Lazy as LazyProperty
34
34
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 from rhodecode.lib.vcs import connection
36 from rhodecode.lib.vcs import connection
37 from rhodecode.lib.vcs.utils import author_name, author_email
37 from rhodecode.lib.vcs.utils import author_name, author_email
38 from rhodecode.lib.vcs.conf import settings
38 from rhodecode.lib.vcs.conf import settings
39 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 RepositoryError)
44 RepositoryError)
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 FILEMODE_DEFAULT = 0100644
50 FILEMODE_DEFAULT = 0100644
51 FILEMODE_EXECUTABLE = 0100755
51 FILEMODE_EXECUTABLE = 0100755
52
52
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 MergeResponse = collections.namedtuple(
54 MergeResponse = collections.namedtuple(
55 'MergeResponse',
55 'MergeResponse',
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57
57
58
58
59 class MergeFailureReason(object):
59 class MergeFailureReason(object):
60 """
60 """
61 Enumeration with all the reasons why the server side merge could fail.
61 Enumeration with all the reasons why the server side merge could fail.
62
62
63 DO NOT change the number of the reasons, as they may be stored in the
63 DO NOT change the number of the reasons, as they may be stored in the
64 database.
64 database.
65
65
66 Changing the name of a reason is acceptable and encouraged to deprecate old
66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 reasons.
67 reasons.
68 """
68 """
69
69
70 # Everything went well.
70 # Everything went well.
71 NONE = 0
71 NONE = 0
72
72
73 # An unexpected exception was raised. Check the logs for more details.
73 # An unexpected exception was raised. Check the logs for more details.
74 UNKNOWN = 1
74 UNKNOWN = 1
75
75
76 # The merge was not successful, there are conflicts.
76 # The merge was not successful, there are conflicts.
77 MERGE_FAILED = 2
77 MERGE_FAILED = 2
78
78
79 # The merge succeeded but we could not push it to the target repository.
79 # The merge succeeded but we could not push it to the target repository.
80 PUSH_FAILED = 3
80 PUSH_FAILED = 3
81
81
82 # The specified target is not a head in the target repository.
82 # The specified target is not a head in the target repository.
83 TARGET_IS_NOT_HEAD = 4
83 TARGET_IS_NOT_HEAD = 4
84
84
85 # The source repository contains more branches than the target. Pushing
85 # The source repository contains more branches than the target. Pushing
86 # the merge will create additional branches in the target.
86 # the merge will create additional branches in the target.
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88
88
89 # The target reference has multiple heads. That does not allow to correctly
89 # The target reference has multiple heads. That does not allow to correctly
90 # identify the target location. This could only happen for mercurial
90 # identify the target location. This could only happen for mercurial
91 # branches.
91 # branches.
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93
93
94 # The target repository is locked
94 # The target repository is locked
95 TARGET_IS_LOCKED = 7
95 TARGET_IS_LOCKED = 7
96
96
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 # A involved commit could not be found.
98 # A involved commit could not be found.
99 _DEPRECATED_MISSING_COMMIT = 8
99 _DEPRECATED_MISSING_COMMIT = 8
100
100
101 # The target repo reference is missing.
101 # The target repo reference is missing.
102 MISSING_TARGET_REF = 9
102 MISSING_TARGET_REF = 9
103
103
104 # The source repo reference is missing.
104 # The source repo reference is missing.
105 MISSING_SOURCE_REF = 10
105 MISSING_SOURCE_REF = 10
106
106
107 # The merge was not successful, there are conflicts related to sub
107 # The merge was not successful, there are conflicts related to sub
108 # repositories.
108 # repositories.
109 SUBREPO_MERGE_FAILED = 11
109 SUBREPO_MERGE_FAILED = 11
110
110
111
111
112 class UpdateFailureReason(object):
112 class UpdateFailureReason(object):
113 """
113 """
114 Enumeration with all the reasons why the pull request update could fail.
114 Enumeration with all the reasons why the pull request update could fail.
115
115
116 DO NOT change the number of the reasons, as they may be stored in the
116 DO NOT change the number of the reasons, as they may be stored in the
117 database.
117 database.
118
118
119 Changing the name of a reason is acceptable and encouraged to deprecate old
119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 reasons.
120 reasons.
121 """
121 """
122
122
123 # Everything went well.
123 # Everything went well.
124 NONE = 0
124 NONE = 0
125
125
126 # An unexpected exception was raised. Check the logs for more details.
126 # An unexpected exception was raised. Check the logs for more details.
127 UNKNOWN = 1
127 UNKNOWN = 1
128
128
129 # The pull request is up to date.
129 # The pull request is up to date.
130 NO_CHANGE = 2
130 NO_CHANGE = 2
131
131
132 # The pull request has a reference type that is not supported for update.
132 # The pull request has a reference type that is not supported for update.
133 WRONG_REF_TPYE = 3
133 WRONG_REF_TPYE = 3
134
134
135 # Update failed because the target reference is missing.
135 # Update failed because the target reference is missing.
136 MISSING_TARGET_REF = 4
136 MISSING_TARGET_REF = 4
137
137
138 # Update failed because the source reference is missing.
138 # Update failed because the source reference is missing.
139 MISSING_SOURCE_REF = 5
139 MISSING_SOURCE_REF = 5
140
140
141
141
142 class BaseRepository(object):
142 class BaseRepository(object):
143 """
143 """
144 Base Repository for final backends
144 Base Repository for final backends
145
145
146 .. attribute:: DEFAULT_BRANCH_NAME
146 .. attribute:: DEFAULT_BRANCH_NAME
147
147
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149
149
150 .. attribute:: commit_ids
150 .. attribute:: commit_ids
151
151
152 list of all available commit ids, in ascending order
152 list of all available commit ids, in ascending order
153
153
154 .. attribute:: path
154 .. attribute:: path
155
155
156 absolute path to the repository
156 absolute path to the repository
157
157
158 .. attribute:: bookmarks
158 .. attribute:: bookmarks
159
159
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 there are no bookmarks or the backend implementation does not support
161 there are no bookmarks or the backend implementation does not support
162 bookmarks.
162 bookmarks.
163
163
164 .. attribute:: tags
164 .. attribute:: tags
165
165
166 Mapping from name to :term:`Commit ID` of the tag.
166 Mapping from name to :term:`Commit ID` of the tag.
167
167
168 """
168 """
169
169
170 DEFAULT_BRANCH_NAME = None
170 DEFAULT_BRANCH_NAME = None
171 DEFAULT_CONTACT = u"Unknown"
171 DEFAULT_CONTACT = u"Unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
173 EMPTY_COMMIT_ID = '0' * 40
173 EMPTY_COMMIT_ID = '0' * 40
174
174
175 path = None
175 path = None
176
176
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 """
178 """
179 Initializes repository. Raises RepositoryError if repository could
179 Initializes repository. Raises RepositoryError if repository could
180 not be find at the given ``repo_path`` or directory at ``repo_path``
180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 exists and ``create`` is set to True.
181 exists and ``create`` is set to True.
182
182
183 :param repo_path: local path of the repository
183 :param repo_path: local path of the repository
184 :param config: repository configuration
184 :param config: repository configuration
185 :param create=False: if set to True, would try to create repository.
185 :param create=False: if set to True, would try to create repository.
186 :param src_url=None: if set, should be proper url from which repository
186 :param src_url=None: if set, should be proper url from which repository
187 would be cloned; requires ``create`` parameter to be set to True -
187 would be cloned; requires ``create`` parameter to be set to True -
188 raises RepositoryError if src_url is set and create evaluates to
188 raises RepositoryError if src_url is set and create evaluates to
189 False
189 False
190 """
190 """
191 raise NotImplementedError
191 raise NotImplementedError
192
192
193 def __repr__(self):
193 def __repr__(self):
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195
195
196 def __len__(self):
196 def __len__(self):
197 return self.count()
197 return self.count()
198
198
199 def __eq__(self, other):
199 def __eq__(self, other):
200 same_instance = isinstance(other, self.__class__)
200 same_instance = isinstance(other, self.__class__)
201 return same_instance and other.path == self.path
201 return same_instance and other.path == self.path
202
202
203 def __ne__(self, other):
203 def __ne__(self, other):
204 return not self.__eq__(other)
204 return not self.__eq__(other)
205
205
206 @LazyProperty
206 @LazyProperty
207 def EMPTY_COMMIT(self):
207 def EMPTY_COMMIT(self):
208 return EmptyCommit(self.EMPTY_COMMIT_ID)
208 return EmptyCommit(self.EMPTY_COMMIT_ID)
209
209
210 @LazyProperty
210 @LazyProperty
211 def alias(self):
211 def alias(self):
212 for k, v in settings.BACKENDS.items():
212 for k, v in settings.BACKENDS.items():
213 if v.split('.')[-1] == str(self.__class__.__name__):
213 if v.split('.')[-1] == str(self.__class__.__name__):
214 return k
214 return k
215
215
216 @LazyProperty
216 @LazyProperty
217 def name(self):
217 def name(self):
218 return safe_unicode(os.path.basename(self.path))
218 return safe_unicode(os.path.basename(self.path))
219
219
220 @LazyProperty
220 @LazyProperty
221 def description(self):
221 def description(self):
222 raise NotImplementedError
222 raise NotImplementedError
223
223
224 def refs(self):
224 def refs(self):
225 """
225 """
226 returns a `dict` with branches, bookmarks, tags, and closed_branches
226 returns a `dict` with branches, bookmarks, tags, and closed_branches
227 for this repository
227 for this repository
228 """
228 """
229 raise NotImplementedError
229 return dict(
230 branches=self.branches,
231 branches_closed=self.branches_closed,
232 tags=self.tags,
233 bookmarks=self.bookmarks
234 )
230
235
231 @LazyProperty
236 @LazyProperty
232 def branches(self):
237 def branches(self):
233 """
238 """
234 A `dict` which maps branch names to commit ids.
239 A `dict` which maps branch names to commit ids.
235 """
240 """
236 raise NotImplementedError
241 raise NotImplementedError
237
242
238 @LazyProperty
243 @LazyProperty
244 def tags(self):
245 """
246 A `dict` which maps tags names to commit ids.
247 """
248 raise NotImplementedError
249
250 @LazyProperty
239 def size(self):
251 def size(self):
240 """
252 """
241 Returns combined size in bytes for all repository files
253 Returns combined size in bytes for all repository files
242 """
254 """
243 tip = self.get_commit()
255 tip = self.get_commit()
244 return tip.size
256 return tip.size
245
257
246 def size_at_commit(self, commit_id):
258 def size_at_commit(self, commit_id):
247 commit = self.get_commit(commit_id)
259 commit = self.get_commit(commit_id)
248 return commit.size
260 return commit.size
249
261
250 def is_empty(self):
262 def is_empty(self):
251 return not bool(self.commit_ids)
263 return not bool(self.commit_ids)
252
264
253 @staticmethod
265 @staticmethod
254 def check_url(url, config):
266 def check_url(url, config):
255 """
267 """
256 Function will check given url and try to verify if it's a valid
268 Function will check given url and try to verify if it's a valid
257 link.
269 link.
258 """
270 """
259 raise NotImplementedError
271 raise NotImplementedError
260
272
261 @staticmethod
273 @staticmethod
262 def is_valid_repository(path):
274 def is_valid_repository(path):
263 """
275 """
264 Check if given `path` contains a valid repository of this backend
276 Check if given `path` contains a valid repository of this backend
265 """
277 """
266 raise NotImplementedError
278 raise NotImplementedError
267
279
268 # ==========================================================================
280 # ==========================================================================
269 # COMMITS
281 # COMMITS
270 # ==========================================================================
282 # ==========================================================================
271
283
272 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
284 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
273 """
285 """
274 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
286 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
275 are both None, most recent commit is returned.
287 are both None, most recent commit is returned.
276
288
277 :param pre_load: Optional. List of commit attributes to load.
289 :param pre_load: Optional. List of commit attributes to load.
278
290
279 :raises ``EmptyRepositoryError``: if there are no commits
291 :raises ``EmptyRepositoryError``: if there are no commits
280 """
292 """
281 raise NotImplementedError
293 raise NotImplementedError
282
294
283 def __iter__(self):
295 def __iter__(self):
284 for commit_id in self.commit_ids:
296 for commit_id in self.commit_ids:
285 yield self.get_commit(commit_id=commit_id)
297 yield self.get_commit(commit_id=commit_id)
286
298
287 def get_commits(
299 def get_commits(
288 self, start_id=None, end_id=None, start_date=None, end_date=None,
300 self, start_id=None, end_id=None, start_date=None, end_date=None,
289 branch_name=None, pre_load=None):
301 branch_name=None, pre_load=None):
290 """
302 """
291 Returns iterator of `BaseCommit` objects from start to end
303 Returns iterator of `BaseCommit` objects from start to end
292 not inclusive. This should behave just like a list, ie. end is not
304 not inclusive. This should behave just like a list, ie. end is not
293 inclusive.
305 inclusive.
294
306
295 :param start_id: None or str, must be a valid commit id
307 :param start_id: None or str, must be a valid commit id
296 :param end_id: None or str, must be a valid commit id
308 :param end_id: None or str, must be a valid commit id
297 :param start_date:
309 :param start_date:
298 :param end_date:
310 :param end_date:
299 :param branch_name:
311 :param branch_name:
300 :param pre_load:
312 :param pre_load:
301 """
313 """
302 raise NotImplementedError
314 raise NotImplementedError
303
315
304 def __getitem__(self, key):
316 def __getitem__(self, key):
305 """
317 """
306 Allows index based access to the commit objects of this repository.
318 Allows index based access to the commit objects of this repository.
307 """
319 """
308 pre_load = ["author", "branch", "date", "message", "parents"]
320 pre_load = ["author", "branch", "date", "message", "parents"]
309 if isinstance(key, slice):
321 if isinstance(key, slice):
310 return self._get_range(key, pre_load)
322 return self._get_range(key, pre_load)
311 return self.get_commit(commit_idx=key, pre_load=pre_load)
323 return self.get_commit(commit_idx=key, pre_load=pre_load)
312
324
313 def _get_range(self, slice_obj, pre_load):
325 def _get_range(self, slice_obj, pre_load):
314 for commit_id in self.commit_ids.__getitem__(slice_obj):
326 for commit_id in self.commit_ids.__getitem__(slice_obj):
315 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
327 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
316
328
317 def count(self):
329 def count(self):
318 return len(self.commit_ids)
330 return len(self.commit_ids)
319
331
320 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
332 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
321 """
333 """
322 Creates and returns a tag for the given ``commit_id``.
334 Creates and returns a tag for the given ``commit_id``.
323
335
324 :param name: name for new tag
336 :param name: name for new tag
325 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
337 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
326 :param commit_id: commit id for which new tag would be created
338 :param commit_id: commit id for which new tag would be created
327 :param message: message of the tag's commit
339 :param message: message of the tag's commit
328 :param date: date of tag's commit
340 :param date: date of tag's commit
329
341
330 :raises TagAlreadyExistError: if tag with same name already exists
342 :raises TagAlreadyExistError: if tag with same name already exists
331 """
343 """
332 raise NotImplementedError
344 raise NotImplementedError
333
345
334 def remove_tag(self, name, user, message=None, date=None):
346 def remove_tag(self, name, user, message=None, date=None):
335 """
347 """
336 Removes tag with the given ``name``.
348 Removes tag with the given ``name``.
337
349
338 :param name: name of the tag to be removed
350 :param name: name of the tag to be removed
339 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
351 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
340 :param message: message of the tag's removal commit
352 :param message: message of the tag's removal commit
341 :param date: date of tag's removal commit
353 :param date: date of tag's removal commit
342
354
343 :raises TagDoesNotExistError: if tag with given name does not exists
355 :raises TagDoesNotExistError: if tag with given name does not exists
344 """
356 """
345 raise NotImplementedError
357 raise NotImplementedError
346
358
347 def get_diff(
359 def get_diff(
348 self, commit1, commit2, path=None, ignore_whitespace=False,
360 self, commit1, commit2, path=None, ignore_whitespace=False,
349 context=3, path1=None):
361 context=3, path1=None):
350 """
362 """
351 Returns (git like) *diff*, as plain text. Shows changes introduced by
363 Returns (git like) *diff*, as plain text. Shows changes introduced by
352 `commit2` since `commit1`.
364 `commit2` since `commit1`.
353
365
354 :param commit1: Entry point from which diff is shown. Can be
366 :param commit1: Entry point from which diff is shown. Can be
355 ``self.EMPTY_COMMIT`` - in this case, patch showing all
367 ``self.EMPTY_COMMIT`` - in this case, patch showing all
356 the changes since empty state of the repository until `commit2`
368 the changes since empty state of the repository until `commit2`
357 :param commit2: Until which commit changes should be shown.
369 :param commit2: Until which commit changes should be shown.
358 :param path: Can be set to a path of a file to create a diff of that
370 :param path: Can be set to a path of a file to create a diff of that
359 file. If `path1` is also set, this value is only associated to
371 file. If `path1` is also set, this value is only associated to
360 `commit2`.
372 `commit2`.
361 :param ignore_whitespace: If set to ``True``, would not show whitespace
373 :param ignore_whitespace: If set to ``True``, would not show whitespace
362 changes. Defaults to ``False``.
374 changes. Defaults to ``False``.
363 :param context: How many lines before/after changed lines should be
375 :param context: How many lines before/after changed lines should be
364 shown. Defaults to ``3``.
376 shown. Defaults to ``3``.
365 :param path1: Can be set to a path to associate with `commit1`. This
377 :param path1: Can be set to a path to associate with `commit1`. This
366 parameter works only for backends which support diff generation for
378 parameter works only for backends which support diff generation for
367 different paths. Other backends will raise a `ValueError` if `path1`
379 different paths. Other backends will raise a `ValueError` if `path1`
368 is set and has a different value than `path`.
380 is set and has a different value than `path`.
369 """
381 """
370 raise NotImplementedError
382 raise NotImplementedError
371
383
372 def strip(self, commit_id, branch=None):
384 def strip(self, commit_id, branch=None):
373 """
385 """
374 Strip given commit_id from the repository
386 Strip given commit_id from the repository
375 """
387 """
376 raise NotImplementedError
388 raise NotImplementedError
377
389
378 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
390 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
379 """
391 """
380 Return a latest common ancestor commit if one exists for this repo
392 Return a latest common ancestor commit if one exists for this repo
381 `commit_id1` vs `commit_id2` from `repo2`.
393 `commit_id1` vs `commit_id2` from `repo2`.
382
394
383 :param commit_id1: Commit it from this repository to use as a
395 :param commit_id1: Commit it from this repository to use as a
384 target for the comparison.
396 target for the comparison.
385 :param commit_id2: Source commit id to use for comparison.
397 :param commit_id2: Source commit id to use for comparison.
386 :param repo2: Source repository to use for comparison.
398 :param repo2: Source repository to use for comparison.
387 """
399 """
388 raise NotImplementedError
400 raise NotImplementedError
389
401
390 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
402 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
391 """
403 """
392 Compare this repository's revision `commit_id1` with `commit_id2`.
404 Compare this repository's revision `commit_id1` with `commit_id2`.
393
405
394 Returns a tuple(commits, ancestor) that would be merged from
406 Returns a tuple(commits, ancestor) that would be merged from
395 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
407 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
396 will be returned as ancestor.
408 will be returned as ancestor.
397
409
398 :param commit_id1: Commit it from this repository to use as a
410 :param commit_id1: Commit it from this repository to use as a
399 target for the comparison.
411 target for the comparison.
400 :param commit_id2: Source commit id to use for comparison.
412 :param commit_id2: Source commit id to use for comparison.
401 :param repo2: Source repository to use for comparison.
413 :param repo2: Source repository to use for comparison.
402 :param merge: If set to ``True`` will do a merge compare which also
414 :param merge: If set to ``True`` will do a merge compare which also
403 returns the common ancestor.
415 returns the common ancestor.
404 :param pre_load: Optional. List of commit attributes to load.
416 :param pre_load: Optional. List of commit attributes to load.
405 """
417 """
406 raise NotImplementedError
418 raise NotImplementedError
407
419
408 def merge(self, target_ref, source_repo, source_ref, workspace_id,
420 def merge(self, target_ref, source_repo, source_ref, workspace_id,
409 user_name='', user_email='', message='', dry_run=False,
421 user_name='', user_email='', message='', dry_run=False,
410 use_rebase=False):
422 use_rebase=False):
411 """
423 """
412 Merge the revisions specified in `source_ref` from `source_repo`
424 Merge the revisions specified in `source_ref` from `source_repo`
413 onto the `target_ref` of this repository.
425 onto the `target_ref` of this repository.
414
426
415 `source_ref` and `target_ref` are named tupls with the following
427 `source_ref` and `target_ref` are named tupls with the following
416 fields `type`, `name` and `commit_id`.
428 fields `type`, `name` and `commit_id`.
417
429
418 Returns a MergeResponse named tuple with the following fields
430 Returns a MergeResponse named tuple with the following fields
419 'possible', 'executed', 'source_commit', 'target_commit',
431 'possible', 'executed', 'source_commit', 'target_commit',
420 'merge_commit'.
432 'merge_commit'.
421
433
422 :param target_ref: `target_ref` points to the commit on top of which
434 :param target_ref: `target_ref` points to the commit on top of which
423 the `source_ref` should be merged.
435 the `source_ref` should be merged.
424 :param source_repo: The repository that contains the commits to be
436 :param source_repo: The repository that contains the commits to be
425 merged.
437 merged.
426 :param source_ref: `source_ref` points to the topmost commit from
438 :param source_ref: `source_ref` points to the topmost commit from
427 the `source_repo` which should be merged.
439 the `source_repo` which should be merged.
428 :param workspace_id: `workspace_id` unique identifier.
440 :param workspace_id: `workspace_id` unique identifier.
429 :param user_name: Merge commit `user_name`.
441 :param user_name: Merge commit `user_name`.
430 :param user_email: Merge commit `user_email`.
442 :param user_email: Merge commit `user_email`.
431 :param message: Merge commit `message`.
443 :param message: Merge commit `message`.
432 :param dry_run: If `True` the merge will not take place.
444 :param dry_run: If `True` the merge will not take place.
433 :param use_rebase: If `True` commits from the source will be rebased
445 :param use_rebase: If `True` commits from the source will be rebased
434 on top of the target instead of being merged.
446 on top of the target instead of being merged.
435 """
447 """
436 if dry_run:
448 if dry_run:
437 message = message or 'dry_run_merge_message'
449 message = message or 'dry_run_merge_message'
438 user_email = user_email or 'dry-run-merge@rhodecode.com'
450 user_email = user_email or 'dry-run-merge@rhodecode.com'
439 user_name = user_name or 'Dry-Run User'
451 user_name = user_name or 'Dry-Run User'
440 else:
452 else:
441 if not user_name:
453 if not user_name:
442 raise ValueError('user_name cannot be empty')
454 raise ValueError('user_name cannot be empty')
443 if not user_email:
455 if not user_email:
444 raise ValueError('user_email cannot be empty')
456 raise ValueError('user_email cannot be empty')
445 if not message:
457 if not message:
446 raise ValueError('message cannot be empty')
458 raise ValueError('message cannot be empty')
447
459
448 shadow_repository_path = self._maybe_prepare_merge_workspace(
460 shadow_repository_path = self._maybe_prepare_merge_workspace(
449 workspace_id, target_ref)
461 workspace_id, target_ref)
450
462
451 try:
463 try:
452 return self._merge_repo(
464 return self._merge_repo(
453 shadow_repository_path, target_ref, source_repo,
465 shadow_repository_path, target_ref, source_repo,
454 source_ref, message, user_name, user_email, dry_run=dry_run,
466 source_ref, message, user_name, user_email, dry_run=dry_run,
455 use_rebase=use_rebase)
467 use_rebase=use_rebase)
456 except RepositoryError:
468 except RepositoryError:
457 log.exception(
469 log.exception(
458 'Unexpected failure when running merge, dry-run=%s',
470 'Unexpected failure when running merge, dry-run=%s',
459 dry_run)
471 dry_run)
460 return MergeResponse(
472 return MergeResponse(
461 False, False, None, MergeFailureReason.UNKNOWN)
473 False, False, None, MergeFailureReason.UNKNOWN)
462
474
463 def _merge_repo(self, shadow_repository_path, target_ref,
475 def _merge_repo(self, shadow_repository_path, target_ref,
464 source_repo, source_ref, merge_message,
476 source_repo, source_ref, merge_message,
465 merger_name, merger_email, dry_run=False, use_rebase=False):
477 merger_name, merger_email, dry_run=False, use_rebase=False):
466 """Internal implementation of merge."""
478 """Internal implementation of merge."""
467 raise NotImplementedError
479 raise NotImplementedError
468
480
469 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
481 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
470 """
482 """
471 Create the merge workspace.
483 Create the merge workspace.
472
484
473 :param workspace_id: `workspace_id` unique identifier.
485 :param workspace_id: `workspace_id` unique identifier.
474 """
486 """
475 raise NotImplementedError
487 raise NotImplementedError
476
488
477 def cleanup_merge_workspace(self, workspace_id):
489 def cleanup_merge_workspace(self, workspace_id):
478 """
490 """
479 Remove merge workspace.
491 Remove merge workspace.
480
492
481 This function MUST not fail in case there is no workspace associated to
493 This function MUST not fail in case there is no workspace associated to
482 the given `workspace_id`.
494 the given `workspace_id`.
483
495
484 :param workspace_id: `workspace_id` unique identifier.
496 :param workspace_id: `workspace_id` unique identifier.
485 """
497 """
486 raise NotImplementedError
498 raise NotImplementedError
487
499
488 # ========== #
500 # ========== #
489 # COMMIT API #
501 # COMMIT API #
490 # ========== #
502 # ========== #
491
503
492 @LazyProperty
504 @LazyProperty
493 def in_memory_commit(self):
505 def in_memory_commit(self):
494 """
506 """
495 Returns :class:`InMemoryCommit` object for this repository.
507 Returns :class:`InMemoryCommit` object for this repository.
496 """
508 """
497 raise NotImplementedError
509 raise NotImplementedError
498
510
499 # ======================== #
511 # ======================== #
500 # UTILITIES FOR SUBCLASSES #
512 # UTILITIES FOR SUBCLASSES #
501 # ======================== #
513 # ======================== #
502
514
503 def _validate_diff_commits(self, commit1, commit2):
515 def _validate_diff_commits(self, commit1, commit2):
504 """
516 """
505 Validates that the given commits are related to this repository.
517 Validates that the given commits are related to this repository.
506
518
507 Intended as a utility for sub classes to have a consistent validation
519 Intended as a utility for sub classes to have a consistent validation
508 of input parameters in methods like :meth:`get_diff`.
520 of input parameters in methods like :meth:`get_diff`.
509 """
521 """
510 self._validate_commit(commit1)
522 self._validate_commit(commit1)
511 self._validate_commit(commit2)
523 self._validate_commit(commit2)
512 if (isinstance(commit1, EmptyCommit) and
524 if (isinstance(commit1, EmptyCommit) and
513 isinstance(commit2, EmptyCommit)):
525 isinstance(commit2, EmptyCommit)):
514 raise ValueError("Cannot compare two empty commits")
526 raise ValueError("Cannot compare two empty commits")
515
527
516 def _validate_commit(self, commit):
528 def _validate_commit(self, commit):
517 if not isinstance(commit, BaseCommit):
529 if not isinstance(commit, BaseCommit):
518 raise TypeError(
530 raise TypeError(
519 "%s is not of type BaseCommit" % repr(commit))
531 "%s is not of type BaseCommit" % repr(commit))
520 if commit.repository != self and not isinstance(commit, EmptyCommit):
532 if commit.repository != self and not isinstance(commit, EmptyCommit):
521 raise ValueError(
533 raise ValueError(
522 "Commit %s must be a valid commit from this repository %s, "
534 "Commit %s must be a valid commit from this repository %s, "
523 "related to this repository instead %s." %
535 "related to this repository instead %s." %
524 (commit, self, commit.repository))
536 (commit, self, commit.repository))
525
537
526 def _validate_commit_id(self, commit_id):
538 def _validate_commit_id(self, commit_id):
527 if not isinstance(commit_id, basestring):
539 if not isinstance(commit_id, basestring):
528 raise TypeError("commit_id must be a string value")
540 raise TypeError("commit_id must be a string value")
529
541
530 def _validate_commit_idx(self, commit_idx):
542 def _validate_commit_idx(self, commit_idx):
531 if not isinstance(commit_idx, (int, long)):
543 if not isinstance(commit_idx, (int, long)):
532 raise TypeError("commit_idx must be a numeric value")
544 raise TypeError("commit_idx must be a numeric value")
533
545
534 def _validate_branch_name(self, branch_name):
546 def _validate_branch_name(self, branch_name):
535 if branch_name and branch_name not in self.branches_all:
547 if branch_name and branch_name not in self.branches_all:
536 msg = ("Branch %s not found in %s" % (branch_name, self))
548 msg = ("Branch %s not found in %s" % (branch_name, self))
537 raise BranchDoesNotExistError(msg)
549 raise BranchDoesNotExistError(msg)
538
550
539 #
551 #
540 # Supporting deprecated API parts
552 # Supporting deprecated API parts
541 # TODO: johbo: consider to move this into a mixin
553 # TODO: johbo: consider to move this into a mixin
542 #
554 #
543
555
544 @property
556 @property
545 def EMPTY_CHANGESET(self):
557 def EMPTY_CHANGESET(self):
546 warnings.warn(
558 warnings.warn(
547 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
559 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
548 return self.EMPTY_COMMIT_ID
560 return self.EMPTY_COMMIT_ID
549
561
550 @property
562 @property
551 def revisions(self):
563 def revisions(self):
552 warnings.warn("Use commits attribute instead", DeprecationWarning)
564 warnings.warn("Use commits attribute instead", DeprecationWarning)
553 return self.commit_ids
565 return self.commit_ids
554
566
555 @revisions.setter
567 @revisions.setter
556 def revisions(self, value):
568 def revisions(self, value):
557 warnings.warn("Use commits attribute instead", DeprecationWarning)
569 warnings.warn("Use commits attribute instead", DeprecationWarning)
558 self.commit_ids = value
570 self.commit_ids = value
559
571
560 def get_changeset(self, revision=None, pre_load=None):
572 def get_changeset(self, revision=None, pre_load=None):
561 warnings.warn("Use get_commit instead", DeprecationWarning)
573 warnings.warn("Use get_commit instead", DeprecationWarning)
562 commit_id = None
574 commit_id = None
563 commit_idx = None
575 commit_idx = None
564 if isinstance(revision, basestring):
576 if isinstance(revision, basestring):
565 commit_id = revision
577 commit_id = revision
566 else:
578 else:
567 commit_idx = revision
579 commit_idx = revision
568 return self.get_commit(
580 return self.get_commit(
569 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
581 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
570
582
571 def get_changesets(
583 def get_changesets(
572 self, start=None, end=None, start_date=None, end_date=None,
584 self, start=None, end=None, start_date=None, end_date=None,
573 branch_name=None, pre_load=None):
585 branch_name=None, pre_load=None):
574 warnings.warn("Use get_commits instead", DeprecationWarning)
586 warnings.warn("Use get_commits instead", DeprecationWarning)
575 start_id = self._revision_to_commit(start)
587 start_id = self._revision_to_commit(start)
576 end_id = self._revision_to_commit(end)
588 end_id = self._revision_to_commit(end)
577 return self.get_commits(
589 return self.get_commits(
578 start_id=start_id, end_id=end_id, start_date=start_date,
590 start_id=start_id, end_id=end_id, start_date=start_date,
579 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
591 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
580
592
581 def _revision_to_commit(self, revision):
593 def _revision_to_commit(self, revision):
582 """
594 """
583 Translates a revision to a commit_id
595 Translates a revision to a commit_id
584
596
585 Helps to support the old changeset based API which allows to use
597 Helps to support the old changeset based API which allows to use
586 commit ids and commit indices interchangeable.
598 commit ids and commit indices interchangeable.
587 """
599 """
588 if revision is None:
600 if revision is None:
589 return revision
601 return revision
590
602
591 if isinstance(revision, basestring):
603 if isinstance(revision, basestring):
592 commit_id = revision
604 commit_id = revision
593 else:
605 else:
594 commit_id = self.commit_ids[revision]
606 commit_id = self.commit_ids[revision]
595 return commit_id
607 return commit_id
596
608
597 @property
609 @property
598 def in_memory_changeset(self):
610 def in_memory_changeset(self):
599 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
611 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
600 return self.in_memory_commit
612 return self.in_memory_commit
601
613
602
614
603 class BaseCommit(object):
615 class BaseCommit(object):
604 """
616 """
605 Each backend should implement it's commit representation.
617 Each backend should implement it's commit representation.
606
618
607 **Attributes**
619 **Attributes**
608
620
609 ``repository``
621 ``repository``
610 repository object within which commit exists
622 repository object within which commit exists
611
623
612 ``id``
624 ``id``
613 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
625 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
614 just ``tip``.
626 just ``tip``.
615
627
616 ``raw_id``
628 ``raw_id``
617 raw commit representation (i.e. full 40 length sha for git
629 raw commit representation (i.e. full 40 length sha for git
618 backend)
630 backend)
619
631
620 ``short_id``
632 ``short_id``
621 shortened (if apply) version of ``raw_id``; it would be simple
633 shortened (if apply) version of ``raw_id``; it would be simple
622 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
634 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
623 as ``raw_id`` for subversion
635 as ``raw_id`` for subversion
624
636
625 ``idx``
637 ``idx``
626 commit index
638 commit index
627
639
628 ``files``
640 ``files``
629 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
641 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
630
642
631 ``dirs``
643 ``dirs``
632 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
644 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
633
645
634 ``nodes``
646 ``nodes``
635 combined list of ``Node`` objects
647 combined list of ``Node`` objects
636
648
637 ``author``
649 ``author``
638 author of the commit, as unicode
650 author of the commit, as unicode
639
651
640 ``message``
652 ``message``
641 message of the commit, as unicode
653 message of the commit, as unicode
642
654
643 ``parents``
655 ``parents``
644 list of parent commits
656 list of parent commits
645
657
646 """
658 """
647
659
648 branch = None
660 branch = None
649 """
661 """
650 Depending on the backend this should be set to the branch name of the
662 Depending on the backend this should be set to the branch name of the
651 commit. Backends not supporting branches on commits should leave this
663 commit. Backends not supporting branches on commits should leave this
652 value as ``None``.
664 value as ``None``.
653 """
665 """
654
666
655 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
667 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
656 """
668 """
657 This template is used to generate a default prefix for repository archives
669 This template is used to generate a default prefix for repository archives
658 if no prefix has been specified.
670 if no prefix has been specified.
659 """
671 """
660
672
661 def __str__(self):
673 def __str__(self):
662 return '<%s at %s:%s>' % (
674 return '<%s at %s:%s>' % (
663 self.__class__.__name__, self.idx, self.short_id)
675 self.__class__.__name__, self.idx, self.short_id)
664
676
665 def __repr__(self):
677 def __repr__(self):
666 return self.__str__()
678 return self.__str__()
667
679
668 def __unicode__(self):
680 def __unicode__(self):
669 return u'%s:%s' % (self.idx, self.short_id)
681 return u'%s:%s' % (self.idx, self.short_id)
670
682
671 def __eq__(self, other):
683 def __eq__(self, other):
672 same_instance = isinstance(other, self.__class__)
684 same_instance = isinstance(other, self.__class__)
673 return same_instance and self.raw_id == other.raw_id
685 return same_instance and self.raw_id == other.raw_id
674
686
675 def __json__(self):
687 def __json__(self):
676 parents = []
688 parents = []
677 try:
689 try:
678 for parent in self.parents:
690 for parent in self.parents:
679 parents.append({'raw_id': parent.raw_id})
691 parents.append({'raw_id': parent.raw_id})
680 except NotImplementedError:
692 except NotImplementedError:
681 # empty commit doesn't have parents implemented
693 # empty commit doesn't have parents implemented
682 pass
694 pass
683
695
684 return {
696 return {
685 'short_id': self.short_id,
697 'short_id': self.short_id,
686 'raw_id': self.raw_id,
698 'raw_id': self.raw_id,
687 'revision': self.idx,
699 'revision': self.idx,
688 'message': self.message,
700 'message': self.message,
689 'date': self.date,
701 'date': self.date,
690 'author': self.author,
702 'author': self.author,
691 'parents': parents,
703 'parents': parents,
692 'branch': self.branch
704 'branch': self.branch
693 }
705 }
694
706
695 @LazyProperty
707 @LazyProperty
696 def last(self):
708 def last(self):
697 """
709 """
698 ``True`` if this is last commit in repository, ``False``
710 ``True`` if this is last commit in repository, ``False``
699 otherwise; trying to access this attribute while there is no
711 otherwise; trying to access this attribute while there is no
700 commits would raise `EmptyRepositoryError`
712 commits would raise `EmptyRepositoryError`
701 """
713 """
702 if self.repository is None:
714 if self.repository is None:
703 raise CommitError("Cannot check if it's most recent commit")
715 raise CommitError("Cannot check if it's most recent commit")
704 return self.raw_id == self.repository.commit_ids[-1]
716 return self.raw_id == self.repository.commit_ids[-1]
705
717
706 @LazyProperty
718 @LazyProperty
707 def parents(self):
719 def parents(self):
708 """
720 """
709 Returns list of parent commits.
721 Returns list of parent commits.
710 """
722 """
711 raise NotImplementedError
723 raise NotImplementedError
712
724
713 @property
725 @property
714 def merge(self):
726 def merge(self):
715 """
727 """
716 Returns boolean if commit is a merge.
728 Returns boolean if commit is a merge.
717 """
729 """
718 return len(self.parents) > 1
730 return len(self.parents) > 1
719
731
720 @LazyProperty
732 @LazyProperty
721 def children(self):
733 def children(self):
722 """
734 """
723 Returns list of child commits.
735 Returns list of child commits.
724 """
736 """
725 raise NotImplementedError
737 raise NotImplementedError
726
738
727 @LazyProperty
739 @LazyProperty
728 def id(self):
740 def id(self):
729 """
741 """
730 Returns string identifying this commit.
742 Returns string identifying this commit.
731 """
743 """
732 raise NotImplementedError
744 raise NotImplementedError
733
745
734 @LazyProperty
746 @LazyProperty
735 def raw_id(self):
747 def raw_id(self):
736 """
748 """
737 Returns raw string identifying this commit.
749 Returns raw string identifying this commit.
738 """
750 """
739 raise NotImplementedError
751 raise NotImplementedError
740
752
741 @LazyProperty
753 @LazyProperty
742 def short_id(self):
754 def short_id(self):
743 """
755 """
744 Returns shortened version of ``raw_id`` attribute, as string,
756 Returns shortened version of ``raw_id`` attribute, as string,
745 identifying this commit, useful for presentation to users.
757 identifying this commit, useful for presentation to users.
746 """
758 """
747 raise NotImplementedError
759 raise NotImplementedError
748
760
749 @LazyProperty
761 @LazyProperty
750 def idx(self):
762 def idx(self):
751 """
763 """
752 Returns integer identifying this commit.
764 Returns integer identifying this commit.
753 """
765 """
754 raise NotImplementedError
766 raise NotImplementedError
755
767
756 @LazyProperty
768 @LazyProperty
757 def committer(self):
769 def committer(self):
758 """
770 """
759 Returns committer for this commit
771 Returns committer for this commit
760 """
772 """
761 raise NotImplementedError
773 raise NotImplementedError
762
774
763 @LazyProperty
775 @LazyProperty
764 def committer_name(self):
776 def committer_name(self):
765 """
777 """
766 Returns committer name for this commit
778 Returns committer name for this commit
767 """
779 """
768
780
769 return author_name(self.committer)
781 return author_name(self.committer)
770
782
771 @LazyProperty
783 @LazyProperty
772 def committer_email(self):
784 def committer_email(self):
773 """
785 """
774 Returns committer email address for this commit
786 Returns committer email address for this commit
775 """
787 """
776
788
777 return author_email(self.committer)
789 return author_email(self.committer)
778
790
779 @LazyProperty
791 @LazyProperty
780 def author(self):
792 def author(self):
781 """
793 """
782 Returns author for this commit
794 Returns author for this commit
783 """
795 """
784
796
785 raise NotImplementedError
797 raise NotImplementedError
786
798
787 @LazyProperty
799 @LazyProperty
788 def author_name(self):
800 def author_name(self):
789 """
801 """
790 Returns author name for this commit
802 Returns author name for this commit
791 """
803 """
792
804
793 return author_name(self.author)
805 return author_name(self.author)
794
806
795 @LazyProperty
807 @LazyProperty
796 def author_email(self):
808 def author_email(self):
797 """
809 """
798 Returns author email address for this commit
810 Returns author email address for this commit
799 """
811 """
800
812
801 return author_email(self.author)
813 return author_email(self.author)
802
814
803 def get_file_mode(self, path):
815 def get_file_mode(self, path):
804 """
816 """
805 Returns stat mode of the file at `path`.
817 Returns stat mode of the file at `path`.
806 """
818 """
807 raise NotImplementedError
819 raise NotImplementedError
808
820
809 def is_link(self, path):
821 def is_link(self, path):
810 """
822 """
811 Returns ``True`` if given `path` is a symlink
823 Returns ``True`` if given `path` is a symlink
812 """
824 """
813 raise NotImplementedError
825 raise NotImplementedError
814
826
815 def get_file_content(self, path):
827 def get_file_content(self, path):
816 """
828 """
817 Returns content of the file at the given `path`.
829 Returns content of the file at the given `path`.
818 """
830 """
819 raise NotImplementedError
831 raise NotImplementedError
820
832
821 def get_file_size(self, path):
833 def get_file_size(self, path):
822 """
834 """
823 Returns size of the file at the given `path`.
835 Returns size of the file at the given `path`.
824 """
836 """
825 raise NotImplementedError
837 raise NotImplementedError
826
838
827 def get_file_commit(self, path, pre_load=None):
839 def get_file_commit(self, path, pre_load=None):
828 """
840 """
829 Returns last commit of the file at the given `path`.
841 Returns last commit of the file at the given `path`.
830
842
831 :param pre_load: Optional. List of commit attributes to load.
843 :param pre_load: Optional. List of commit attributes to load.
832 """
844 """
833 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
845 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
834 if not commits:
846 if not commits:
835 raise RepositoryError(
847 raise RepositoryError(
836 'Failed to fetch history for path {}. '
848 'Failed to fetch history for path {}. '
837 'Please check if such path exists in your repository'.format(
849 'Please check if such path exists in your repository'.format(
838 path))
850 path))
839 return commits[0]
851 return commits[0]
840
852
841 def get_file_history(self, path, limit=None, pre_load=None):
853 def get_file_history(self, path, limit=None, pre_load=None):
842 """
854 """
843 Returns history of file as reversed list of :class:`BaseCommit`
855 Returns history of file as reversed list of :class:`BaseCommit`
844 objects for which file at given `path` has been modified.
856 objects for which file at given `path` has been modified.
845
857
846 :param limit: Optional. Allows to limit the size of the returned
858 :param limit: Optional. Allows to limit the size of the returned
847 history. This is intended as a hint to the underlying backend, so
859 history. This is intended as a hint to the underlying backend, so
848 that it can apply optimizations depending on the limit.
860 that it can apply optimizations depending on the limit.
849 :param pre_load: Optional. List of commit attributes to load.
861 :param pre_load: Optional. List of commit attributes to load.
850 """
862 """
851 raise NotImplementedError
863 raise NotImplementedError
852
864
853 def get_file_annotate(self, path, pre_load=None):
865 def get_file_annotate(self, path, pre_load=None):
854 """
866 """
855 Returns a generator of four element tuples with
867 Returns a generator of four element tuples with
856 lineno, sha, commit lazy loader and line
868 lineno, sha, commit lazy loader and line
857
869
858 :param pre_load: Optional. List of commit attributes to load.
870 :param pre_load: Optional. List of commit attributes to load.
859 """
871 """
860 raise NotImplementedError
872 raise NotImplementedError
861
873
862 def get_nodes(self, path):
874 def get_nodes(self, path):
863 """
875 """
864 Returns combined ``DirNode`` and ``FileNode`` objects list representing
876 Returns combined ``DirNode`` and ``FileNode`` objects list representing
865 state of commit at the given ``path``.
877 state of commit at the given ``path``.
866
878
867 :raises ``CommitError``: if node at the given ``path`` is not
879 :raises ``CommitError``: if node at the given ``path`` is not
868 instance of ``DirNode``
880 instance of ``DirNode``
869 """
881 """
870 raise NotImplementedError
882 raise NotImplementedError
871
883
872 def get_node(self, path):
884 def get_node(self, path):
873 """
885 """
874 Returns ``Node`` object from the given ``path``.
886 Returns ``Node`` object from the given ``path``.
875
887
876 :raises ``NodeDoesNotExistError``: if there is no node at the given
888 :raises ``NodeDoesNotExistError``: if there is no node at the given
877 ``path``
889 ``path``
878 """
890 """
879 raise NotImplementedError
891 raise NotImplementedError
880
892
881 def get_largefile_node(self, path):
893 def get_largefile_node(self, path):
882 """
894 """
883 Returns the path to largefile from Mercurial storage.
895 Returns the path to largefile from Mercurial storage.
884 """
896 """
885 raise NotImplementedError
897 raise NotImplementedError
886
898
887 def archive_repo(self, file_path, kind='tgz', subrepos=None,
899 def archive_repo(self, file_path, kind='tgz', subrepos=None,
888 prefix=None, write_metadata=False, mtime=None):
900 prefix=None, write_metadata=False, mtime=None):
889 """
901 """
890 Creates an archive containing the contents of the repository.
902 Creates an archive containing the contents of the repository.
891
903
892 :param file_path: path to the file which to create the archive.
904 :param file_path: path to the file which to create the archive.
893 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
905 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
894 :param prefix: name of root directory in archive.
906 :param prefix: name of root directory in archive.
895 Default is repository name and commit's short_id joined with dash:
907 Default is repository name and commit's short_id joined with dash:
896 ``"{repo_name}-{short_id}"``.
908 ``"{repo_name}-{short_id}"``.
897 :param write_metadata: write a metadata file into archive.
909 :param write_metadata: write a metadata file into archive.
898 :param mtime: custom modification time for archive creation, defaults
910 :param mtime: custom modification time for archive creation, defaults
899 to time.time() if not given.
911 to time.time() if not given.
900
912
901 :raise VCSError: If prefix has a problem.
913 :raise VCSError: If prefix has a problem.
902 """
914 """
903 allowed_kinds = settings.ARCHIVE_SPECS.keys()
915 allowed_kinds = settings.ARCHIVE_SPECS.keys()
904 if kind not in allowed_kinds:
916 if kind not in allowed_kinds:
905 raise ImproperArchiveTypeError(
917 raise ImproperArchiveTypeError(
906 'Archive kind (%s) not supported use one of %s' %
918 'Archive kind (%s) not supported use one of %s' %
907 (kind, allowed_kinds))
919 (kind, allowed_kinds))
908
920
909 prefix = self._validate_archive_prefix(prefix)
921 prefix = self._validate_archive_prefix(prefix)
910
922
911 mtime = mtime or time.mktime(self.date.timetuple())
923 mtime = mtime or time.mktime(self.date.timetuple())
912
924
913 file_info = []
925 file_info = []
914 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
926 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
915 for _r, _d, files in cur_rev.walk('/'):
927 for _r, _d, files in cur_rev.walk('/'):
916 for f in files:
928 for f in files:
917 f_path = os.path.join(prefix, f.path)
929 f_path = os.path.join(prefix, f.path)
918 file_info.append(
930 file_info.append(
919 (f_path, f.mode, f.is_link(), f.raw_bytes))
931 (f_path, f.mode, f.is_link(), f.raw_bytes))
920
932
921 if write_metadata:
933 if write_metadata:
922 metadata = [
934 metadata = [
923 ('repo_name', self.repository.name),
935 ('repo_name', self.repository.name),
924 ('rev', self.raw_id),
936 ('rev', self.raw_id),
925 ('create_time', mtime),
937 ('create_time', mtime),
926 ('branch', self.branch),
938 ('branch', self.branch),
927 ('tags', ','.join(self.tags)),
939 ('tags', ','.join(self.tags)),
928 ]
940 ]
929 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
941 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
930 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
942 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
931
943
932 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
944 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
933
945
934 def _validate_archive_prefix(self, prefix):
946 def _validate_archive_prefix(self, prefix):
935 if prefix is None:
947 if prefix is None:
936 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
948 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
937 repo_name=safe_str(self.repository.name),
949 repo_name=safe_str(self.repository.name),
938 short_id=self.short_id)
950 short_id=self.short_id)
939 elif not isinstance(prefix, str):
951 elif not isinstance(prefix, str):
940 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
952 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
941 elif prefix.startswith('/'):
953 elif prefix.startswith('/'):
942 raise VCSError("Prefix cannot start with leading slash")
954 raise VCSError("Prefix cannot start with leading slash")
943 elif prefix.strip() == '':
955 elif prefix.strip() == '':
944 raise VCSError("Prefix cannot be empty")
956 raise VCSError("Prefix cannot be empty")
945 return prefix
957 return prefix
946
958
947 @LazyProperty
959 @LazyProperty
948 def root(self):
960 def root(self):
949 """
961 """
950 Returns ``RootNode`` object for this commit.
962 Returns ``RootNode`` object for this commit.
951 """
963 """
952 return self.get_node('')
964 return self.get_node('')
953
965
954 def next(self, branch=None):
966 def next(self, branch=None):
955 """
967 """
956 Returns next commit from current, if branch is gives it will return
968 Returns next commit from current, if branch is gives it will return
957 next commit belonging to this branch
969 next commit belonging to this branch
958
970
959 :param branch: show commits within the given named branch
971 :param branch: show commits within the given named branch
960 """
972 """
961 indexes = xrange(self.idx + 1, self.repository.count())
973 indexes = xrange(self.idx + 1, self.repository.count())
962 return self._find_next(indexes, branch)
974 return self._find_next(indexes, branch)
963
975
964 def prev(self, branch=None):
976 def prev(self, branch=None):
965 """
977 """
966 Returns previous commit from current, if branch is gives it will
978 Returns previous commit from current, if branch is gives it will
967 return previous commit belonging to this branch
979 return previous commit belonging to this branch
968
980
969 :param branch: show commit within the given named branch
981 :param branch: show commit within the given named branch
970 """
982 """
971 indexes = xrange(self.idx - 1, -1, -1)
983 indexes = xrange(self.idx - 1, -1, -1)
972 return self._find_next(indexes, branch)
984 return self._find_next(indexes, branch)
973
985
974 def _find_next(self, indexes, branch=None):
986 def _find_next(self, indexes, branch=None):
975 if branch and self.branch != branch:
987 if branch and self.branch != branch:
976 raise VCSError('Branch option used on commit not belonging '
988 raise VCSError('Branch option used on commit not belonging '
977 'to that branch')
989 'to that branch')
978
990
979 for next_idx in indexes:
991 for next_idx in indexes:
980 commit = self.repository.get_commit(commit_idx=next_idx)
992 commit = self.repository.get_commit(commit_idx=next_idx)
981 if branch and branch != commit.branch:
993 if branch and branch != commit.branch:
982 continue
994 continue
983 return commit
995 return commit
984 raise CommitDoesNotExistError
996 raise CommitDoesNotExistError
985
997
986 def diff(self, ignore_whitespace=True, context=3):
998 def diff(self, ignore_whitespace=True, context=3):
987 """
999 """
988 Returns a `Diff` object representing the change made by this commit.
1000 Returns a `Diff` object representing the change made by this commit.
989 """
1001 """
990 parent = (
1002 parent = (
991 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1003 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
992 diff = self.repository.get_diff(
1004 diff = self.repository.get_diff(
993 parent, self,
1005 parent, self,
994 ignore_whitespace=ignore_whitespace,
1006 ignore_whitespace=ignore_whitespace,
995 context=context)
1007 context=context)
996 return diff
1008 return diff
997
1009
998 @LazyProperty
1010 @LazyProperty
999 def added(self):
1011 def added(self):
1000 """
1012 """
1001 Returns list of added ``FileNode`` objects.
1013 Returns list of added ``FileNode`` objects.
1002 """
1014 """
1003 raise NotImplementedError
1015 raise NotImplementedError
1004
1016
1005 @LazyProperty
1017 @LazyProperty
1006 def changed(self):
1018 def changed(self):
1007 """
1019 """
1008 Returns list of modified ``FileNode`` objects.
1020 Returns list of modified ``FileNode`` objects.
1009 """
1021 """
1010 raise NotImplementedError
1022 raise NotImplementedError
1011
1023
1012 @LazyProperty
1024 @LazyProperty
1013 def removed(self):
1025 def removed(self):
1014 """
1026 """
1015 Returns list of removed ``FileNode`` objects.
1027 Returns list of removed ``FileNode`` objects.
1016 """
1028 """
1017 raise NotImplementedError
1029 raise NotImplementedError
1018
1030
1019 @LazyProperty
1031 @LazyProperty
1020 def size(self):
1032 def size(self):
1021 """
1033 """
1022 Returns total number of bytes from contents of all filenodes.
1034 Returns total number of bytes from contents of all filenodes.
1023 """
1035 """
1024 return sum((node.size for node in self.get_filenodes_generator()))
1036 return sum((node.size for node in self.get_filenodes_generator()))
1025
1037
1026 def walk(self, topurl=''):
1038 def walk(self, topurl=''):
1027 """
1039 """
1028 Similar to os.walk method. Insted of filesystem it walks through
1040 Similar to os.walk method. Insted of filesystem it walks through
1029 commit starting at given ``topurl``. Returns generator of tuples
1041 commit starting at given ``topurl``. Returns generator of tuples
1030 (topnode, dirnodes, filenodes).
1042 (topnode, dirnodes, filenodes).
1031 """
1043 """
1032 topnode = self.get_node(topurl)
1044 topnode = self.get_node(topurl)
1033 if not topnode.is_dir():
1045 if not topnode.is_dir():
1034 return
1046 return
1035 yield (topnode, topnode.dirs, topnode.files)
1047 yield (topnode, topnode.dirs, topnode.files)
1036 for dirnode in topnode.dirs:
1048 for dirnode in topnode.dirs:
1037 for tup in self.walk(dirnode.path):
1049 for tup in self.walk(dirnode.path):
1038 yield tup
1050 yield tup
1039
1051
1040 def get_filenodes_generator(self):
1052 def get_filenodes_generator(self):
1041 """
1053 """
1042 Returns generator that yields *all* file nodes.
1054 Returns generator that yields *all* file nodes.
1043 """
1055 """
1044 for topnode, dirs, files in self.walk():
1056 for topnode, dirs, files in self.walk():
1045 for node in files:
1057 for node in files:
1046 yield node
1058 yield node
1047
1059
1048 #
1060 #
1049 # Utilities for sub classes to support consistent behavior
1061 # Utilities for sub classes to support consistent behavior
1050 #
1062 #
1051
1063
1052 def no_node_at_path(self, path):
1064 def no_node_at_path(self, path):
1053 return NodeDoesNotExistError(
1065 return NodeDoesNotExistError(
1054 "There is no file nor directory at the given path: "
1066 "There is no file nor directory at the given path: "
1055 "'%s' at commit %s" % (path, self.short_id))
1067 "'%s' at commit %s" % (path, self.short_id))
1056
1068
1057 def _fix_path(self, path):
1069 def _fix_path(self, path):
1058 """
1070 """
1059 Paths are stored without trailing slash so we need to get rid off it if
1071 Paths are stored without trailing slash so we need to get rid off it if
1060 needed.
1072 needed.
1061 """
1073 """
1062 return path.rstrip('/')
1074 return path.rstrip('/')
1063
1075
1064 #
1076 #
1065 # Deprecated API based on changesets
1077 # Deprecated API based on changesets
1066 #
1078 #
1067
1079
1068 @property
1080 @property
1069 def revision(self):
1081 def revision(self):
1070 warnings.warn("Use idx instead", DeprecationWarning)
1082 warnings.warn("Use idx instead", DeprecationWarning)
1071 return self.idx
1083 return self.idx
1072
1084
1073 @revision.setter
1085 @revision.setter
1074 def revision(self, value):
1086 def revision(self, value):
1075 warnings.warn("Use idx instead", DeprecationWarning)
1087 warnings.warn("Use idx instead", DeprecationWarning)
1076 self.idx = value
1088 self.idx = value
1077
1089
1078 def get_file_changeset(self, path):
1090 def get_file_changeset(self, path):
1079 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1091 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1080 return self.get_file_commit(path)
1092 return self.get_file_commit(path)
1081
1093
1082
1094
1083 class BaseChangesetClass(type):
1095 class BaseChangesetClass(type):
1084
1096
1085 def __instancecheck__(self, instance):
1097 def __instancecheck__(self, instance):
1086 return isinstance(instance, BaseCommit)
1098 return isinstance(instance, BaseCommit)
1087
1099
1088
1100
1089 class BaseChangeset(BaseCommit):
1101 class BaseChangeset(BaseCommit):
1090
1102
1091 __metaclass__ = BaseChangesetClass
1103 __metaclass__ = BaseChangesetClass
1092
1104
1093 def __new__(cls, *args, **kwargs):
1105 def __new__(cls, *args, **kwargs):
1094 warnings.warn(
1106 warnings.warn(
1095 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1107 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1096 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1108 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1097
1109
1098
1110
1099 class BaseInMemoryCommit(object):
1111 class BaseInMemoryCommit(object):
1100 """
1112 """
1101 Represents differences between repository's state (most recent head) and
1113 Represents differences between repository's state (most recent head) and
1102 changes made *in place*.
1114 changes made *in place*.
1103
1115
1104 **Attributes**
1116 **Attributes**
1105
1117
1106 ``repository``
1118 ``repository``
1107 repository object for this in-memory-commit
1119 repository object for this in-memory-commit
1108
1120
1109 ``added``
1121 ``added``
1110 list of ``FileNode`` objects marked as *added*
1122 list of ``FileNode`` objects marked as *added*
1111
1123
1112 ``changed``
1124 ``changed``
1113 list of ``FileNode`` objects marked as *changed*
1125 list of ``FileNode`` objects marked as *changed*
1114
1126
1115 ``removed``
1127 ``removed``
1116 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1128 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1117 *removed*
1129 *removed*
1118
1130
1119 ``parents``
1131 ``parents``
1120 list of :class:`BaseCommit` instances representing parents of
1132 list of :class:`BaseCommit` instances representing parents of
1121 in-memory commit. Should always be 2-element sequence.
1133 in-memory commit. Should always be 2-element sequence.
1122
1134
1123 """
1135 """
1124
1136
1125 def __init__(self, repository):
1137 def __init__(self, repository):
1126 self.repository = repository
1138 self.repository = repository
1127 self.added = []
1139 self.added = []
1128 self.changed = []
1140 self.changed = []
1129 self.removed = []
1141 self.removed = []
1130 self.parents = []
1142 self.parents = []
1131
1143
1132 def add(self, *filenodes):
1144 def add(self, *filenodes):
1133 """
1145 """
1134 Marks given ``FileNode`` objects as *to be committed*.
1146 Marks given ``FileNode`` objects as *to be committed*.
1135
1147
1136 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1148 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1137 latest commit
1149 latest commit
1138 :raises ``NodeAlreadyAddedError``: if node with same path is already
1150 :raises ``NodeAlreadyAddedError``: if node with same path is already
1139 marked as *added*
1151 marked as *added*
1140 """
1152 """
1141 # Check if not already marked as *added* first
1153 # Check if not already marked as *added* first
1142 for node in filenodes:
1154 for node in filenodes:
1143 if node.path in (n.path for n in self.added):
1155 if node.path in (n.path for n in self.added):
1144 raise NodeAlreadyAddedError(
1156 raise NodeAlreadyAddedError(
1145 "Such FileNode %s is already marked for addition"
1157 "Such FileNode %s is already marked for addition"
1146 % node.path)
1158 % node.path)
1147 for node in filenodes:
1159 for node in filenodes:
1148 self.added.append(node)
1160 self.added.append(node)
1149
1161
1150 def change(self, *filenodes):
1162 def change(self, *filenodes):
1151 """
1163 """
1152 Marks given ``FileNode`` objects to be *changed* in next commit.
1164 Marks given ``FileNode`` objects to be *changed* in next commit.
1153
1165
1154 :raises ``EmptyRepositoryError``: if there are no commits yet
1166 :raises ``EmptyRepositoryError``: if there are no commits yet
1155 :raises ``NodeAlreadyExistsError``: if node with same path is already
1167 :raises ``NodeAlreadyExistsError``: if node with same path is already
1156 marked to be *changed*
1168 marked to be *changed*
1157 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1169 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1158 marked to be *removed*
1170 marked to be *removed*
1159 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1171 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1160 commit
1172 commit
1161 :raises ``NodeNotChangedError``: if node hasn't really be changed
1173 :raises ``NodeNotChangedError``: if node hasn't really be changed
1162 """
1174 """
1163 for node in filenodes:
1175 for node in filenodes:
1164 if node.path in (n.path for n in self.removed):
1176 if node.path in (n.path for n in self.removed):
1165 raise NodeAlreadyRemovedError(
1177 raise NodeAlreadyRemovedError(
1166 "Node at %s is already marked as removed" % node.path)
1178 "Node at %s is already marked as removed" % node.path)
1167 try:
1179 try:
1168 self.repository.get_commit()
1180 self.repository.get_commit()
1169 except EmptyRepositoryError:
1181 except EmptyRepositoryError:
1170 raise EmptyRepositoryError(
1182 raise EmptyRepositoryError(
1171 "Nothing to change - try to *add* new nodes rather than "
1183 "Nothing to change - try to *add* new nodes rather than "
1172 "changing them")
1184 "changing them")
1173 for node in filenodes:
1185 for node in filenodes:
1174 if node.path in (n.path for n in self.changed):
1186 if node.path in (n.path for n in self.changed):
1175 raise NodeAlreadyChangedError(
1187 raise NodeAlreadyChangedError(
1176 "Node at '%s' is already marked as changed" % node.path)
1188 "Node at '%s' is already marked as changed" % node.path)
1177 self.changed.append(node)
1189 self.changed.append(node)
1178
1190
1179 def remove(self, *filenodes):
1191 def remove(self, *filenodes):
1180 """
1192 """
1181 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1193 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1182 *removed* in next commit.
1194 *removed* in next commit.
1183
1195
1184 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1196 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1185 be *removed*
1197 be *removed*
1186 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1198 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1187 be *changed*
1199 be *changed*
1188 """
1200 """
1189 for node in filenodes:
1201 for node in filenodes:
1190 if node.path in (n.path for n in self.removed):
1202 if node.path in (n.path for n in self.removed):
1191 raise NodeAlreadyRemovedError(
1203 raise NodeAlreadyRemovedError(
1192 "Node is already marked to for removal at %s" % node.path)
1204 "Node is already marked to for removal at %s" % node.path)
1193 if node.path in (n.path for n in self.changed):
1205 if node.path in (n.path for n in self.changed):
1194 raise NodeAlreadyChangedError(
1206 raise NodeAlreadyChangedError(
1195 "Node is already marked to be changed at %s" % node.path)
1207 "Node is already marked to be changed at %s" % node.path)
1196 # We only mark node as *removed* - real removal is done by
1208 # We only mark node as *removed* - real removal is done by
1197 # commit method
1209 # commit method
1198 self.removed.append(node)
1210 self.removed.append(node)
1199
1211
1200 def reset(self):
1212 def reset(self):
1201 """
1213 """
1202 Resets this instance to initial state (cleans ``added``, ``changed``
1214 Resets this instance to initial state (cleans ``added``, ``changed``
1203 and ``removed`` lists).
1215 and ``removed`` lists).
1204 """
1216 """
1205 self.added = []
1217 self.added = []
1206 self.changed = []
1218 self.changed = []
1207 self.removed = []
1219 self.removed = []
1208 self.parents = []
1220 self.parents = []
1209
1221
1210 def get_ipaths(self):
1222 def get_ipaths(self):
1211 """
1223 """
1212 Returns generator of paths from nodes marked as added, changed or
1224 Returns generator of paths from nodes marked as added, changed or
1213 removed.
1225 removed.
1214 """
1226 """
1215 for node in itertools.chain(self.added, self.changed, self.removed):
1227 for node in itertools.chain(self.added, self.changed, self.removed):
1216 yield node.path
1228 yield node.path
1217
1229
1218 def get_paths(self):
1230 def get_paths(self):
1219 """
1231 """
1220 Returns list of paths from nodes marked as added, changed or removed.
1232 Returns list of paths from nodes marked as added, changed or removed.
1221 """
1233 """
1222 return list(self.get_ipaths())
1234 return list(self.get_ipaths())
1223
1235
1224 def check_integrity(self, parents=None):
1236 def check_integrity(self, parents=None):
1225 """
1237 """
1226 Checks in-memory commit's integrity. Also, sets parents if not
1238 Checks in-memory commit's integrity. Also, sets parents if not
1227 already set.
1239 already set.
1228
1240
1229 :raises CommitError: if any error occurs (i.e.
1241 :raises CommitError: if any error occurs (i.e.
1230 ``NodeDoesNotExistError``).
1242 ``NodeDoesNotExistError``).
1231 """
1243 """
1232 if not self.parents:
1244 if not self.parents:
1233 parents = parents or []
1245 parents = parents or []
1234 if len(parents) == 0:
1246 if len(parents) == 0:
1235 try:
1247 try:
1236 parents = [self.repository.get_commit(), None]
1248 parents = [self.repository.get_commit(), None]
1237 except EmptyRepositoryError:
1249 except EmptyRepositoryError:
1238 parents = [None, None]
1250 parents = [None, None]
1239 elif len(parents) == 1:
1251 elif len(parents) == 1:
1240 parents += [None]
1252 parents += [None]
1241 self.parents = parents
1253 self.parents = parents
1242
1254
1243 # Local parents, only if not None
1255 # Local parents, only if not None
1244 parents = [p for p in self.parents if p]
1256 parents = [p for p in self.parents if p]
1245
1257
1246 # Check nodes marked as added
1258 # Check nodes marked as added
1247 for p in parents:
1259 for p in parents:
1248 for node in self.added:
1260 for node in self.added:
1249 try:
1261 try:
1250 p.get_node(node.path)
1262 p.get_node(node.path)
1251 except NodeDoesNotExistError:
1263 except NodeDoesNotExistError:
1252 pass
1264 pass
1253 else:
1265 else:
1254 raise NodeAlreadyExistsError(
1266 raise NodeAlreadyExistsError(
1255 "Node `%s` already exists at %s" % (node.path, p))
1267 "Node `%s` already exists at %s" % (node.path, p))
1256
1268
1257 # Check nodes marked as changed
1269 # Check nodes marked as changed
1258 missing = set(self.changed)
1270 missing = set(self.changed)
1259 not_changed = set(self.changed)
1271 not_changed = set(self.changed)
1260 if self.changed and not parents:
1272 if self.changed and not parents:
1261 raise NodeDoesNotExistError(str(self.changed[0].path))
1273 raise NodeDoesNotExistError(str(self.changed[0].path))
1262 for p in parents:
1274 for p in parents:
1263 for node in self.changed:
1275 for node in self.changed:
1264 try:
1276 try:
1265 old = p.get_node(node.path)
1277 old = p.get_node(node.path)
1266 missing.remove(node)
1278 missing.remove(node)
1267 # if content actually changed, remove node from not_changed
1279 # if content actually changed, remove node from not_changed
1268 if old.content != node.content:
1280 if old.content != node.content:
1269 not_changed.remove(node)
1281 not_changed.remove(node)
1270 except NodeDoesNotExistError:
1282 except NodeDoesNotExistError:
1271 pass
1283 pass
1272 if self.changed and missing:
1284 if self.changed and missing:
1273 raise NodeDoesNotExistError(
1285 raise NodeDoesNotExistError(
1274 "Node `%s` marked as modified but missing in parents: %s"
1286 "Node `%s` marked as modified but missing in parents: %s"
1275 % (node.path, parents))
1287 % (node.path, parents))
1276
1288
1277 if self.changed and not_changed:
1289 if self.changed and not_changed:
1278 raise NodeNotChangedError(
1290 raise NodeNotChangedError(
1279 "Node `%s` wasn't actually changed (parents: %s)"
1291 "Node `%s` wasn't actually changed (parents: %s)"
1280 % (not_changed.pop().path, parents))
1292 % (not_changed.pop().path, parents))
1281
1293
1282 # Check nodes marked as removed
1294 # Check nodes marked as removed
1283 if self.removed and not parents:
1295 if self.removed and not parents:
1284 raise NodeDoesNotExistError(
1296 raise NodeDoesNotExistError(
1285 "Cannot remove node at %s as there "
1297 "Cannot remove node at %s as there "
1286 "were no parents specified" % self.removed[0].path)
1298 "were no parents specified" % self.removed[0].path)
1287 really_removed = set()
1299 really_removed = set()
1288 for p in parents:
1300 for p in parents:
1289 for node in self.removed:
1301 for node in self.removed:
1290 try:
1302 try:
1291 p.get_node(node.path)
1303 p.get_node(node.path)
1292 really_removed.add(node)
1304 really_removed.add(node)
1293 except CommitError:
1305 except CommitError:
1294 pass
1306 pass
1295 not_removed = set(self.removed) - really_removed
1307 not_removed = set(self.removed) - really_removed
1296 if not_removed:
1308 if not_removed:
1297 # TODO: johbo: This code branch does not seem to be covered
1309 # TODO: johbo: This code branch does not seem to be covered
1298 raise NodeDoesNotExistError(
1310 raise NodeDoesNotExistError(
1299 "Cannot remove node at %s from "
1311 "Cannot remove node at %s from "
1300 "following parents: %s" % (not_removed, parents))
1312 "following parents: %s" % (not_removed, parents))
1301
1313
1302 def commit(
1314 def commit(
1303 self, message, author, parents=None, branch=None, date=None,
1315 self, message, author, parents=None, branch=None, date=None,
1304 **kwargs):
1316 **kwargs):
1305 """
1317 """
1306 Performs in-memory commit (doesn't check workdir in any way) and
1318 Performs in-memory commit (doesn't check workdir in any way) and
1307 returns newly created :class:`BaseCommit`. Updates repository's
1319 returns newly created :class:`BaseCommit`. Updates repository's
1308 attribute `commits`.
1320 attribute `commits`.
1309
1321
1310 .. note::
1322 .. note::
1311
1323
1312 While overriding this method each backend's should call
1324 While overriding this method each backend's should call
1313 ``self.check_integrity(parents)`` in the first place.
1325 ``self.check_integrity(parents)`` in the first place.
1314
1326
1315 :param message: message of the commit
1327 :param message: message of the commit
1316 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1328 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1317 :param parents: single parent or sequence of parents from which commit
1329 :param parents: single parent or sequence of parents from which commit
1318 would be derived
1330 would be derived
1319 :param date: ``datetime.datetime`` instance. Defaults to
1331 :param date: ``datetime.datetime`` instance. Defaults to
1320 ``datetime.datetime.now()``.
1332 ``datetime.datetime.now()``.
1321 :param branch: branch name, as string. If none given, default backend's
1333 :param branch: branch name, as string. If none given, default backend's
1322 branch would be used.
1334 branch would be used.
1323
1335
1324 :raises ``CommitError``: if any error occurs while committing
1336 :raises ``CommitError``: if any error occurs while committing
1325 """
1337 """
1326 raise NotImplementedError
1338 raise NotImplementedError
1327
1339
1328
1340
1329 class BaseInMemoryChangesetClass(type):
1341 class BaseInMemoryChangesetClass(type):
1330
1342
1331 def __instancecheck__(self, instance):
1343 def __instancecheck__(self, instance):
1332 return isinstance(instance, BaseInMemoryCommit)
1344 return isinstance(instance, BaseInMemoryCommit)
1333
1345
1334
1346
1335 class BaseInMemoryChangeset(BaseInMemoryCommit):
1347 class BaseInMemoryChangeset(BaseInMemoryCommit):
1336
1348
1337 __metaclass__ = BaseInMemoryChangesetClass
1349 __metaclass__ = BaseInMemoryChangesetClass
1338
1350
1339 def __new__(cls, *args, **kwargs):
1351 def __new__(cls, *args, **kwargs):
1340 warnings.warn(
1352 warnings.warn(
1341 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1353 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1342 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1354 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1343
1355
1344
1356
1345 class EmptyCommit(BaseCommit):
1357 class EmptyCommit(BaseCommit):
1346 """
1358 """
1347 An dummy empty commit. It's possible to pass hash when creating
1359 An dummy empty commit. It's possible to pass hash when creating
1348 an EmptyCommit
1360 an EmptyCommit
1349 """
1361 """
1350
1362
1351 def __init__(
1363 def __init__(
1352 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1364 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1353 message='', author='', date=None):
1365 message='', author='', date=None):
1354 self._empty_commit_id = commit_id
1366 self._empty_commit_id = commit_id
1355 # TODO: johbo: Solve idx parameter, default value does not make
1367 # TODO: johbo: Solve idx parameter, default value does not make
1356 # too much sense
1368 # too much sense
1357 self.idx = idx
1369 self.idx = idx
1358 self.message = message
1370 self.message = message
1359 self.author = author
1371 self.author = author
1360 self.date = date or datetime.datetime.fromtimestamp(0)
1372 self.date = date or datetime.datetime.fromtimestamp(0)
1361 self.repository = repo
1373 self.repository = repo
1362 self.alias = alias
1374 self.alias = alias
1363
1375
1364 @LazyProperty
1376 @LazyProperty
1365 def raw_id(self):
1377 def raw_id(self):
1366 """
1378 """
1367 Returns raw string identifying this commit, useful for web
1379 Returns raw string identifying this commit, useful for web
1368 representation.
1380 representation.
1369 """
1381 """
1370
1382
1371 return self._empty_commit_id
1383 return self._empty_commit_id
1372
1384
1373 @LazyProperty
1385 @LazyProperty
1374 def branch(self):
1386 def branch(self):
1375 if self.alias:
1387 if self.alias:
1376 from rhodecode.lib.vcs.backends import get_backend
1388 from rhodecode.lib.vcs.backends import get_backend
1377 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1389 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1378
1390
1379 @LazyProperty
1391 @LazyProperty
1380 def short_id(self):
1392 def short_id(self):
1381 return self.raw_id[:12]
1393 return self.raw_id[:12]
1382
1394
1383 @LazyProperty
1395 @LazyProperty
1384 def id(self):
1396 def id(self):
1385 return self.raw_id
1397 return self.raw_id
1386
1398
1387 def get_file_commit(self, path):
1399 def get_file_commit(self, path):
1388 return self
1400 return self
1389
1401
1390 def get_file_content(self, path):
1402 def get_file_content(self, path):
1391 return u''
1403 return u''
1392
1404
1393 def get_file_size(self, path):
1405 def get_file_size(self, path):
1394 return 0
1406 return 0
1395
1407
1396
1408
1397 class EmptyChangesetClass(type):
1409 class EmptyChangesetClass(type):
1398
1410
1399 def __instancecheck__(self, instance):
1411 def __instancecheck__(self, instance):
1400 return isinstance(instance, EmptyCommit)
1412 return isinstance(instance, EmptyCommit)
1401
1413
1402
1414
1403 class EmptyChangeset(EmptyCommit):
1415 class EmptyChangeset(EmptyCommit):
1404
1416
1405 __metaclass__ = EmptyChangesetClass
1417 __metaclass__ = EmptyChangesetClass
1406
1418
1407 def __new__(cls, *args, **kwargs):
1419 def __new__(cls, *args, **kwargs):
1408 warnings.warn(
1420 warnings.warn(
1409 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1421 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1410 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1422 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1411
1423
1412 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1424 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1413 alias=None, revision=-1, message='', author='', date=None):
1425 alias=None, revision=-1, message='', author='', date=None):
1414 if requested_revision is not None:
1426 if requested_revision is not None:
1415 warnings.warn(
1427 warnings.warn(
1416 "Parameter requested_revision not supported anymore",
1428 "Parameter requested_revision not supported anymore",
1417 DeprecationWarning)
1429 DeprecationWarning)
1418 super(EmptyChangeset, self).__init__(
1430 super(EmptyChangeset, self).__init__(
1419 commit_id=cs, repo=repo, alias=alias, idx=revision,
1431 commit_id=cs, repo=repo, alias=alias, idx=revision,
1420 message=message, author=author, date=date)
1432 message=message, author=author, date=date)
1421
1433
1422 @property
1434 @property
1423 def revision(self):
1435 def revision(self):
1424 warnings.warn("Use idx instead", DeprecationWarning)
1436 warnings.warn("Use idx instead", DeprecationWarning)
1425 return self.idx
1437 return self.idx
1426
1438
1427 @revision.setter
1439 @revision.setter
1428 def revision(self, value):
1440 def revision(self, value):
1429 warnings.warn("Use idx instead", DeprecationWarning)
1441 warnings.warn("Use idx instead", DeprecationWarning)
1430 self.idx = value
1442 self.idx = value
1431
1443
1432
1444
1433 class CollectionGenerator(object):
1445 class CollectionGenerator(object):
1434
1446
1435 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1447 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1436 self.repo = repo
1448 self.repo = repo
1437 self.commit_ids = commit_ids
1449 self.commit_ids = commit_ids
1438 # TODO: (oliver) this isn't currently hooked up
1450 # TODO: (oliver) this isn't currently hooked up
1439 self.collection_size = None
1451 self.collection_size = None
1440 self.pre_load = pre_load
1452 self.pre_load = pre_load
1441
1453
1442 def __len__(self):
1454 def __len__(self):
1443 if self.collection_size is not None:
1455 if self.collection_size is not None:
1444 return self.collection_size
1456 return self.collection_size
1445 return self.commit_ids.__len__()
1457 return self.commit_ids.__len__()
1446
1458
1447 def __iter__(self):
1459 def __iter__(self):
1448 for commit_id in self.commit_ids:
1460 for commit_id in self.commit_ids:
1449 # TODO: johbo: Mercurial passes in commit indices or commit ids
1461 # TODO: johbo: Mercurial passes in commit indices or commit ids
1450 yield self._commit_factory(commit_id)
1462 yield self._commit_factory(commit_id)
1451
1463
1452 def _commit_factory(self, commit_id):
1464 def _commit_factory(self, commit_id):
1453 """
1465 """
1454 Allows backends to override the way commits are generated.
1466 Allows backends to override the way commits are generated.
1455 """
1467 """
1456 return self.repo.get_commit(commit_id=commit_id,
1468 return self.repo.get_commit(commit_id=commit_id,
1457 pre_load=self.pre_load)
1469 pre_load=self.pre_load)
1458
1470
1459 def __getslice__(self, i, j):
1471 def __getslice__(self, i, j):
1460 """
1472 """
1461 Returns an iterator of sliced repository
1473 Returns an iterator of sliced repository
1462 """
1474 """
1463 commit_ids = self.commit_ids[i:j]
1475 commit_ids = self.commit_ids[i:j]
1464 return self.__class__(
1476 return self.__class__(
1465 self.repo, commit_ids, pre_load=self.pre_load)
1477 self.repo, commit_ids, pre_load=self.pre_load)
1466
1478
1467 def __repr__(self):
1479 def __repr__(self):
1468 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1480 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1469
1481
1470
1482
1471 class Config(object):
1483 class Config(object):
1472 """
1484 """
1473 Represents the configuration for a repository.
1485 Represents the configuration for a repository.
1474
1486
1475 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1487 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1476 standard library. It implements only the needed subset.
1488 standard library. It implements only the needed subset.
1477 """
1489 """
1478
1490
1479 def __init__(self):
1491 def __init__(self):
1480 self._values = {}
1492 self._values = {}
1481
1493
1482 def copy(self):
1494 def copy(self):
1483 clone = Config()
1495 clone = Config()
1484 for section, values in self._values.items():
1496 for section, values in self._values.items():
1485 clone._values[section] = values.copy()
1497 clone._values[section] = values.copy()
1486 return clone
1498 return clone
1487
1499
1488 def __repr__(self):
1500 def __repr__(self):
1489 return '<Config(%s sections) at %s>' % (
1501 return '<Config(%s sections) at %s>' % (
1490 len(self._values), hex(id(self)))
1502 len(self._values), hex(id(self)))
1491
1503
1492 def items(self, section):
1504 def items(self, section):
1493 return self._values.get(section, {}).iteritems()
1505 return self._values.get(section, {}).iteritems()
1494
1506
1495 def get(self, section, option):
1507 def get(self, section, option):
1496 return self._values.get(section, {}).get(option)
1508 return self._values.get(section, {}).get(option)
1497
1509
1498 def set(self, section, option, value):
1510 def set(self, section, option, value):
1499 section_values = self._values.setdefault(section, {})
1511 section_values = self._values.setdefault(section, {})
1500 section_values[option] = value
1512 section_values[option] = value
1501
1513
1502 def clear_section(self, section):
1514 def clear_section(self, section):
1503 self._values[section] = {}
1515 self._values[section] = {}
1504
1516
1505 def serialize(self):
1517 def serialize(self):
1506 """
1518 """
1507 Creates a list of three tuples (section, key, value) representing
1519 Creates a list of three tuples (section, key, value) representing
1508 this config object.
1520 this config object.
1509 """
1521 """
1510 items = []
1522 items = []
1511 for section in self._values:
1523 for section in self._values:
1512 for option, value in self._values[section].items():
1524 for option, value in self._values[section].items():
1513 items.append(
1525 items.append(
1514 (safe_str(section), safe_str(option), safe_str(value)))
1526 (safe_str(section), safe_str(option), safe_str(value)))
1515 return items
1527 return items
1516
1528
1517
1529
1518 class Diff(object):
1530 class Diff(object):
1519 """
1531 """
1520 Represents a diff result from a repository backend.
1532 Represents a diff result from a repository backend.
1521
1533
1522 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1534 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1523 """
1535 """
1524
1536
1525 _header_re = None
1537 _header_re = None
1526
1538
1527 def __init__(self, raw_diff):
1539 def __init__(self, raw_diff):
1528 self.raw = raw_diff
1540 self.raw = raw_diff
1529
1541
1530 def chunks(self):
1542 def chunks(self):
1531 """
1543 """
1532 split the diff in chunks of separate --git a/file b/file chunks
1544 split the diff in chunks of separate --git a/file b/file chunks
1533 to make diffs consistent we must prepend with \n, and make sure
1545 to make diffs consistent we must prepend with \n, and make sure
1534 we can detect last chunk as this was also has special rule
1546 we can detect last chunk as this was also has special rule
1535 """
1547 """
1536 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1548 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1537 total_chunks = len(chunks)
1549 total_chunks = len(chunks)
1538 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1550 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1539 for cur_chunk, chunk in enumerate(chunks, start=1))
1551 for cur_chunk, chunk in enumerate(chunks, start=1))
1540
1552
1541
1553
1542 class DiffChunk(object):
1554 class DiffChunk(object):
1543
1555
1544 def __init__(self, chunk, diff, last_chunk):
1556 def __init__(self, chunk, diff, last_chunk):
1545 self._diff = diff
1557 self._diff = diff
1546
1558
1547 # since we split by \ndiff --git that part is lost from original diff
1559 # since we split by \ndiff --git that part is lost from original diff
1548 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1560 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1549 if not last_chunk:
1561 if not last_chunk:
1550 chunk += '\n'
1562 chunk += '\n'
1551
1563
1552 match = self._diff._header_re.match(chunk)
1564 match = self._diff._header_re.match(chunk)
1553 self.header = match.groupdict()
1565 self.header = match.groupdict()
1554 self.diff = chunk[match.end():]
1566 self.diff = chunk[match.end():]
1555 self.raw = chunk
1567 self.raw = chunk
General Comments 0
You need to be logged in to leave comments. Login now