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