##// END OF EJS Templates
pull-requests: use close action with proper --close-commit solution....
marcink -
r2056:678ed378 default
parent child Browse files
Show More
@@ -1,1045 +1,1047 b''
1 .. _repo-methods-ref:
1 .. _repo-methods-ref:
2
2
3 repo methods
3 repo methods
4 ============
4 ============
5
5
6 add_field_to_repo
6 add_field_to_repo
7 -----------------
7 -----------------
8
8
9 .. py:function:: add_field_to_repo(apiuser, repoid, key, label=<Optional:''>, description=<Optional:''>)
9 .. py:function:: add_field_to_repo(apiuser, repoid, key, label=<Optional:''>, description=<Optional:''>)
10
10
11 Adds an extra field to a repository.
11 Adds an extra field to a repository.
12
12
13 This command can only be run using an |authtoken| with at least
13 This command can only be run using an |authtoken| with at least
14 write permissions to the |repo|.
14 write permissions to the |repo|.
15
15
16 :param apiuser: This is filled automatically from the |authtoken|.
16 :param apiuser: This is filled automatically from the |authtoken|.
17 :type apiuser: AuthUser
17 :type apiuser: AuthUser
18 :param repoid: Set the repository name or repository id.
18 :param repoid: Set the repository name or repository id.
19 :type repoid: str or int
19 :type repoid: str or int
20 :param key: Create a unique field key for this repository.
20 :param key: Create a unique field key for this repository.
21 :type key: str
21 :type key: str
22 :param label:
22 :param label:
23 :type label: Optional(str)
23 :type label: Optional(str)
24 :param description:
24 :param description:
25 :type description: Optional(str)
25 :type description: Optional(str)
26
26
27
27
28 comment_commit
28 comment_commit
29 --------------
29 --------------
30
30
31 .. py:function:: comment_commit(apiuser, repoid, commit_id, message, status=<Optional:None>, comment_type=<Optional:u'note'>, resolves_comment_id=<Optional:None>, userid=<Optional:<OptionalAttr:apiuser>>)
31 .. py:function:: comment_commit(apiuser, repoid, commit_id, message, status=<Optional:None>, comment_type=<Optional:u'note'>, resolves_comment_id=<Optional:None>, userid=<Optional:<OptionalAttr:apiuser>>)
32
32
33 Set a commit comment, and optionally change the status of the commit.
33 Set a commit comment, and optionally change the status of the commit.
34
34
35 :param apiuser: This is filled automatically from the |authtoken|.
35 :param apiuser: This is filled automatically from the |authtoken|.
36 :type apiuser: AuthUser
36 :type apiuser: AuthUser
37 :param repoid: Set the repository name or repository ID.
37 :param repoid: Set the repository name or repository ID.
38 :type repoid: str or int
38 :type repoid: str or int
39 :param commit_id: Specify the commit_id for which to set a comment.
39 :param commit_id: Specify the commit_id for which to set a comment.
40 :type commit_id: str
40 :type commit_id: str
41 :param message: The comment text.
41 :param message: The comment text.
42 :type message: str
42 :type message: str
43 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
43 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
44 'approved', 'rejected', 'under_review'
44 'approved', 'rejected', 'under_review'
45 :type status: str
45 :type status: str
46 :param comment_type: Comment type, one of: 'note', 'todo'
46 :param comment_type: Comment type, one of: 'note', 'todo'
47 :type comment_type: Optional(str), default: 'note'
47 :type comment_type: Optional(str), default: 'note'
48 :param userid: Set the user name of the comment creator.
48 :param userid: Set the user name of the comment creator.
49 :type userid: Optional(str or int)
49 :type userid: Optional(str or int)
50
50
51 Example error output:
51 Example error output:
52
52
53 .. code-block:: bash
53 .. code-block:: bash
54
54
55 {
55 {
56 "id" : <id_given_in_input>,
56 "id" : <id_given_in_input>,
57 "result" : {
57 "result" : {
58 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
58 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
59 "status_change": null or <status>,
59 "status_change": null or <status>,
60 "success": true
60 "success": true
61 },
61 },
62 "error" : null
62 "error" : null
63 }
63 }
64
64
65
65
66 create_repo
66 create_repo
67 -----------
67 -----------
68
68
69 .. py:function:: create_repo(apiuser, repo_name, repo_type, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, copy_permissions=<Optional:False>)
69 .. py:function:: create_repo(apiuser, repo_name, repo_type, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, copy_permissions=<Optional:False>)
70
70
71 Creates a repository.
71 Creates a repository.
72
72
73 * If the repository name contains "/", repository will be created inside
73 * If the repository name contains "/", repository will be created inside
74 a repository group or nested repository groups
74 a repository group or nested repository groups
75
75
76 For example "foo/bar/repo1" will create |repo| called "repo1" inside
76 For example "foo/bar/repo1" will create |repo| called "repo1" inside
77 group "foo/bar". You have to have permissions to access and write to
77 group "foo/bar". You have to have permissions to access and write to
78 the last repository group ("bar" in this example)
78 the last repository group ("bar" in this example)
79
79
80 This command can only be run using an |authtoken| with at least
80 This command can only be run using an |authtoken| with at least
81 permissions to create repositories, or write permissions to
81 permissions to create repositories, or write permissions to
82 parent repository groups.
82 parent repository groups.
83
83
84 :param apiuser: This is filled automatically from the |authtoken|.
84 :param apiuser: This is filled automatically from the |authtoken|.
85 :type apiuser: AuthUser
85 :type apiuser: AuthUser
86 :param repo_name: Set the repository name.
86 :param repo_name: Set the repository name.
87 :type repo_name: str
87 :type repo_name: str
88 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
88 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
89 :type repo_type: str
89 :type repo_type: str
90 :param owner: user_id or username
90 :param owner: user_id or username
91 :type owner: Optional(str)
91 :type owner: Optional(str)
92 :param description: Set the repository description.
92 :param description: Set the repository description.
93 :type description: Optional(str)
93 :type description: Optional(str)
94 :param private: set repository as private
94 :param private: set repository as private
95 :type private: bool
95 :type private: bool
96 :param clone_uri: set clone_uri
96 :param clone_uri: set clone_uri
97 :type clone_uri: str
97 :type clone_uri: str
98 :param landing_rev: <rev_type>:<rev>
98 :param landing_rev: <rev_type>:<rev>
99 :type landing_rev: str
99 :type landing_rev: str
100 :param enable_locking:
100 :param enable_locking:
101 :type enable_locking: bool
101 :type enable_locking: bool
102 :param enable_downloads:
102 :param enable_downloads:
103 :type enable_downloads: bool
103 :type enable_downloads: bool
104 :param enable_statistics:
104 :param enable_statistics:
105 :type enable_statistics: bool
105 :type enable_statistics: bool
106 :param copy_permissions: Copy permission from group in which the
106 :param copy_permissions: Copy permission from group in which the
107 repository is being created.
107 repository is being created.
108 :type copy_permissions: bool
108 :type copy_permissions: bool
109
109
110
110
111 Example output:
111 Example output:
112
112
113 .. code-block:: bash
113 .. code-block:: bash
114
114
115 id : <id_given_in_input>
115 id : <id_given_in_input>
116 result: {
116 result: {
117 "msg": "Created new repository `<reponame>`",
117 "msg": "Created new repository `<reponame>`",
118 "success": true,
118 "success": true,
119 "task": "<celery task id or None if done sync>"
119 "task": "<celery task id or None if done sync>"
120 }
120 }
121 error: null
121 error: null
122
122
123
123
124 Example error output:
124 Example error output:
125
125
126 .. code-block:: bash
126 .. code-block:: bash
127
127
128 id : <id_given_in_input>
128 id : <id_given_in_input>
129 result : null
129 result : null
130 error : {
130 error : {
131 'failed to create repository `<repo_name>`'
131 'failed to create repository `<repo_name>`'
132 }
132 }
133
133
134
134
135 delete_repo
135 delete_repo
136 -----------
136 -----------
137
137
138 .. py:function:: delete_repo(apiuser, repoid, forks=<Optional:''>)
138 .. py:function:: delete_repo(apiuser, repoid, forks=<Optional:''>)
139
139
140 Deletes a repository.
140 Deletes a repository.
141
141
142 * When the `forks` parameter is set it's possible to detach or delete
142 * When the `forks` parameter is set it's possible to detach or delete
143 forks of deleted repository.
143 forks of deleted repository.
144
144
145 This command can only be run using an |authtoken| with admin
145 This command can only be run using an |authtoken| with admin
146 permissions on the |repo|.
146 permissions on the |repo|.
147
147
148 :param apiuser: This is filled automatically from the |authtoken|.
148 :param apiuser: This is filled automatically from the |authtoken|.
149 :type apiuser: AuthUser
149 :type apiuser: AuthUser
150 :param repoid: Set the repository name or repository ID.
150 :param repoid: Set the repository name or repository ID.
151 :type repoid: str or int
151 :type repoid: str or int
152 :param forks: Set to `detach` or `delete` forks from the |repo|.
152 :param forks: Set to `detach` or `delete` forks from the |repo|.
153 :type forks: Optional(str)
153 :type forks: Optional(str)
154
154
155 Example error output:
155 Example error output:
156
156
157 .. code-block:: bash
157 .. code-block:: bash
158
158
159 id : <id_given_in_input>
159 id : <id_given_in_input>
160 result: {
160 result: {
161 "msg": "Deleted repository `<reponame>`",
161 "msg": "Deleted repository `<reponame>`",
162 "success": true
162 "success": true
163 }
163 }
164 error: null
164 error: null
165
165
166
166
167 fork_repo
167 fork_repo
168 ---------
168 ---------
169
169
170 .. py:function:: fork_repo(apiuser, repoid, fork_name, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, copy_permissions=<Optional:False>)
170 .. py:function:: fork_repo(apiuser, repoid, fork_name, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, copy_permissions=<Optional:False>)
171
171
172 Creates a fork of the specified |repo|.
172 Creates a fork of the specified |repo|.
173
173
174 * If the fork_name contains "/", fork will be created inside
174 * If the fork_name contains "/", fork will be created inside
175 a repository group or nested repository groups
175 a repository group or nested repository groups
176
176
177 For example "foo/bar/fork-repo" will create fork called "fork-repo"
177 For example "foo/bar/fork-repo" will create fork called "fork-repo"
178 inside group "foo/bar". You have to have permissions to access and
178 inside group "foo/bar". You have to have permissions to access and
179 write to the last repository group ("bar" in this example)
179 write to the last repository group ("bar" in this example)
180
180
181 This command can only be run using an |authtoken| with minimum
181 This command can only be run using an |authtoken| with minimum
182 read permissions of the forked repo, create fork permissions for an user.
182 read permissions of the forked repo, create fork permissions for an user.
183
183
184 :param apiuser: This is filled automatically from the |authtoken|.
184 :param apiuser: This is filled automatically from the |authtoken|.
185 :type apiuser: AuthUser
185 :type apiuser: AuthUser
186 :param repoid: Set repository name or repository ID.
186 :param repoid: Set repository name or repository ID.
187 :type repoid: str or int
187 :type repoid: str or int
188 :param fork_name: Set the fork name, including it's repository group membership.
188 :param fork_name: Set the fork name, including it's repository group membership.
189 :type fork_name: str
189 :type fork_name: str
190 :param owner: Set the fork owner.
190 :param owner: Set the fork owner.
191 :type owner: str
191 :type owner: str
192 :param description: Set the fork description.
192 :param description: Set the fork description.
193 :type description: str
193 :type description: str
194 :param copy_permissions: Copy permissions from parent |repo|. The
194 :param copy_permissions: Copy permissions from parent |repo|. The
195 default is False.
195 default is False.
196 :type copy_permissions: bool
196 :type copy_permissions: bool
197 :param private: Make the fork private. The default is False.
197 :param private: Make the fork private. The default is False.
198 :type private: bool
198 :type private: bool
199 :param landing_rev: Set the landing revision. The default is tip.
199 :param landing_rev: Set the landing revision. The default is tip.
200
200
201 Example output:
201 Example output:
202
202
203 .. code-block:: bash
203 .. code-block:: bash
204
204
205 id : <id_for_response>
205 id : <id_for_response>
206 api_key : "<api_key>"
206 api_key : "<api_key>"
207 args: {
207 args: {
208 "repoid" : "<reponame or repo_id>",
208 "repoid" : "<reponame or repo_id>",
209 "fork_name": "<forkname>",
209 "fork_name": "<forkname>",
210 "owner": "<username or user_id = Optional(=apiuser)>",
210 "owner": "<username or user_id = Optional(=apiuser)>",
211 "description": "<description>",
211 "description": "<description>",
212 "copy_permissions": "<bool>",
212 "copy_permissions": "<bool>",
213 "private": "<bool>",
213 "private": "<bool>",
214 "landing_rev": "<landing_rev>"
214 "landing_rev": "<landing_rev>"
215 }
215 }
216
216
217 Example error output:
217 Example error output:
218
218
219 .. code-block:: bash
219 .. code-block:: bash
220
220
221 id : <id_given_in_input>
221 id : <id_given_in_input>
222 result: {
222 result: {
223 "msg": "Created fork of `<reponame>` as `<forkname>`",
223 "msg": "Created fork of `<reponame>` as `<forkname>`",
224 "success": true,
224 "success": true,
225 "task": "<celery task id or None if done sync>"
225 "task": "<celery task id or None if done sync>"
226 }
226 }
227 error: null
227 error: null
228
228
229
229
230 get_repo
230 get_repo
231 --------
231 --------
232
232
233 .. py:function:: get_repo(apiuser, repoid, cache=<Optional:True>)
233 .. py:function:: get_repo(apiuser, repoid, cache=<Optional:True>)
234
234
235 Gets an existing repository by its name or repository_id.
235 Gets an existing repository by its name or repository_id.
236
236
237 The members section so the output returns users groups or users
237 The members section so the output returns users groups or users
238 associated with that repository.
238 associated with that repository.
239
239
240 This command can only be run using an |authtoken| with admin rights,
240 This command can only be run using an |authtoken| with admin rights,
241 or users with at least read rights to the |repo|.
241 or users with at least read rights to the |repo|.
242
242
243 :param apiuser: This is filled automatically from the |authtoken|.
243 :param apiuser: This is filled automatically from the |authtoken|.
244 :type apiuser: AuthUser
244 :type apiuser: AuthUser
245 :param repoid: The repository name or repository id.
245 :param repoid: The repository name or repository id.
246 :type repoid: str or int
246 :type repoid: str or int
247 :param cache: use the cached value for last changeset
247 :param cache: use the cached value for last changeset
248 :type: cache: Optional(bool)
248 :type: cache: Optional(bool)
249
249
250 Example output:
250 Example output:
251
251
252 .. code-block:: bash
252 .. code-block:: bash
253
253
254 {
254 {
255 "error": null,
255 "error": null,
256 "id": <repo_id>,
256 "id": <repo_id>,
257 "result": {
257 "result": {
258 "clone_uri": null,
258 "clone_uri": null,
259 "created_on": "timestamp",
259 "created_on": "timestamp",
260 "description": "repo description",
260 "description": "repo description",
261 "enable_downloads": false,
261 "enable_downloads": false,
262 "enable_locking": false,
262 "enable_locking": false,
263 "enable_statistics": false,
263 "enable_statistics": false,
264 "followers": [
264 "followers": [
265 {
265 {
266 "active": true,
266 "active": true,
267 "admin": false,
267 "admin": false,
268 "api_key": "****************************************",
268 "api_key": "****************************************",
269 "api_keys": [
269 "api_keys": [
270 "****************************************"
270 "****************************************"
271 ],
271 ],
272 "email": "user@example.com",
272 "email": "user@example.com",
273 "emails": [
273 "emails": [
274 "user@example.com"
274 "user@example.com"
275 ],
275 ],
276 "extern_name": "rhodecode",
276 "extern_name": "rhodecode",
277 "extern_type": "rhodecode",
277 "extern_type": "rhodecode",
278 "firstname": "username",
278 "firstname": "username",
279 "ip_addresses": [],
279 "ip_addresses": [],
280 "language": null,
280 "language": null,
281 "last_login": "2015-09-16T17:16:35.854",
281 "last_login": "2015-09-16T17:16:35.854",
282 "lastname": "surname",
282 "lastname": "surname",
283 "user_id": <user_id>,
283 "user_id": <user_id>,
284 "username": "name"
284 "username": "name"
285 }
285 }
286 ],
286 ],
287 "fork_of": "parent-repo",
287 "fork_of": "parent-repo",
288 "landing_rev": [
288 "landing_rev": [
289 "rev",
289 "rev",
290 "tip"
290 "tip"
291 ],
291 ],
292 "last_changeset": {
292 "last_changeset": {
293 "author": "User <user@example.com>",
293 "author": "User <user@example.com>",
294 "branch": "default",
294 "branch": "default",
295 "date": "timestamp",
295 "date": "timestamp",
296 "message": "last commit message",
296 "message": "last commit message",
297 "parents": [
297 "parents": [
298 {
298 {
299 "raw_id": "commit-id"
299 "raw_id": "commit-id"
300 }
300 }
301 ],
301 ],
302 "raw_id": "commit-id",
302 "raw_id": "commit-id",
303 "revision": <revision number>,
303 "revision": <revision number>,
304 "short_id": "short id"
304 "short_id": "short id"
305 },
305 },
306 "lock_reason": null,
306 "lock_reason": null,
307 "locked_by": null,
307 "locked_by": null,
308 "locked_date": null,
308 "locked_date": null,
309 "members": [
309 "members": [
310 {
310 {
311 "name": "super-admin-name",
311 "name": "super-admin-name",
312 "origin": "super-admin",
312 "origin": "super-admin",
313 "permission": "repository.admin",
313 "permission": "repository.admin",
314 "type": "user"
314 "type": "user"
315 },
315 },
316 {
316 {
317 "name": "owner-name",
317 "name": "owner-name",
318 "origin": "owner",
318 "origin": "owner",
319 "permission": "repository.admin",
319 "permission": "repository.admin",
320 "type": "user"
320 "type": "user"
321 },
321 },
322 {
322 {
323 "name": "user-group-name",
323 "name": "user-group-name",
324 "origin": "permission",
324 "origin": "permission",
325 "permission": "repository.write",
325 "permission": "repository.write",
326 "type": "user_group"
326 "type": "user_group"
327 }
327 }
328 ],
328 ],
329 "owner": "owner-name",
329 "owner": "owner-name",
330 "permissions": [
330 "permissions": [
331 {
331 {
332 "name": "super-admin-name",
332 "name": "super-admin-name",
333 "origin": "super-admin",
333 "origin": "super-admin",
334 "permission": "repository.admin",
334 "permission": "repository.admin",
335 "type": "user"
335 "type": "user"
336 },
336 },
337 {
337 {
338 "name": "owner-name",
338 "name": "owner-name",
339 "origin": "owner",
339 "origin": "owner",
340 "permission": "repository.admin",
340 "permission": "repository.admin",
341 "type": "user"
341 "type": "user"
342 },
342 },
343 {
343 {
344 "name": "user-group-name",
344 "name": "user-group-name",
345 "origin": "permission",
345 "origin": "permission",
346 "permission": "repository.write",
346 "permission": "repository.write",
347 "type": "user_group"
347 "type": "user_group"
348 }
348 }
349 ],
349 ],
350 "private": true,
350 "private": true,
351 "repo_id": 676,
351 "repo_id": 676,
352 "repo_name": "user-group/repo-name",
352 "repo_name": "user-group/repo-name",
353 "repo_type": "hg"
353 "repo_type": "hg"
354 }
354 }
355 }
355 }
356
356
357
357
358 get_repo_changeset
358 get_repo_changeset
359 ------------------
359 ------------------
360
360
361 .. py:function:: get_repo_changeset(apiuser, repoid, revision, details=<Optional:'basic'>)
361 .. py:function:: get_repo_changeset(apiuser, repoid, revision, details=<Optional:'basic'>)
362
362
363 Returns information about a changeset.
363 Returns information about a changeset.
364
364
365 Additionally parameters define the amount of details returned by
365 Additionally parameters define the amount of details returned by
366 this function.
366 this function.
367
367
368 This command can only be run using an |authtoken| with admin rights,
368 This command can only be run using an |authtoken| with admin rights,
369 or users with at least read rights to the |repo|.
369 or users with at least read rights to the |repo|.
370
370
371 :param apiuser: This is filled automatically from the |authtoken|.
371 :param apiuser: This is filled automatically from the |authtoken|.
372 :type apiuser: AuthUser
372 :type apiuser: AuthUser
373 :param repoid: The repository name or repository id
373 :param repoid: The repository name or repository id
374 :type repoid: str or int
374 :type repoid: str or int
375 :param revision: revision for which listing should be done
375 :param revision: revision for which listing should be done
376 :type revision: str
376 :type revision: str
377 :param details: details can be 'basic|extended|full' full gives diff
377 :param details: details can be 'basic|extended|full' full gives diff
378 info details like the diff itself, and number of changed files etc.
378 info details like the diff itself, and number of changed files etc.
379 :type details: Optional(str)
379 :type details: Optional(str)
380
380
381
381
382 get_repo_changesets
382 get_repo_changesets
383 -------------------
383 -------------------
384
384
385 .. py:function:: get_repo_changesets(apiuser, repoid, start_rev, limit, details=<Optional:'basic'>)
385 .. py:function:: get_repo_changesets(apiuser, repoid, start_rev, limit, details=<Optional:'basic'>)
386
386
387 Returns a set of commits limited by the number starting
387 Returns a set of commits limited by the number starting
388 from the `start_rev` option.
388 from the `start_rev` option.
389
389
390 Additional parameters define the amount of details returned by this
390 Additional parameters define the amount of details returned by this
391 function.
391 function.
392
392
393 This command can only be run using an |authtoken| with admin rights,
393 This command can only be run using an |authtoken| with admin rights,
394 or users with at least read rights to |repos|.
394 or users with at least read rights to |repos|.
395
395
396 :param apiuser: This is filled automatically from the |authtoken|.
396 :param apiuser: This is filled automatically from the |authtoken|.
397 :type apiuser: AuthUser
397 :type apiuser: AuthUser
398 :param repoid: The repository name or repository ID.
398 :param repoid: The repository name or repository ID.
399 :type repoid: str or int
399 :type repoid: str or int
400 :param start_rev: The starting revision from where to get changesets.
400 :param start_rev: The starting revision from where to get changesets.
401 :type start_rev: str
401 :type start_rev: str
402 :param limit: Limit the number of commits to this amount
402 :param limit: Limit the number of commits to this amount
403 :type limit: str or int
403 :type limit: str or int
404 :param details: Set the level of detail returned. Valid option are:
404 :param details: Set the level of detail returned. Valid option are:
405 ``basic``, ``extended`` and ``full``.
405 ``basic``, ``extended`` and ``full``.
406 :type details: Optional(str)
406 :type details: Optional(str)
407
407
408 .. note::
408 .. note::
409
409
410 Setting the parameter `details` to the value ``full`` is extensive
410 Setting the parameter `details` to the value ``full`` is extensive
411 and returns details like the diff itself, and the number
411 and returns details like the diff itself, and the number
412 of changed files.
412 of changed files.
413
413
414
414
415 get_repo_nodes
415 get_repo_nodes
416 --------------
416 --------------
417
417
418 .. py:function:: get_repo_nodes(apiuser, repoid, revision, root_path, ret_type=<Optional:'all'>, details=<Optional:'basic'>, max_file_bytes=<Optional:None>)
418 .. py:function:: get_repo_nodes(apiuser, repoid, revision, root_path, ret_type=<Optional:'all'>, details=<Optional:'basic'>, max_file_bytes=<Optional:None>)
419
419
420 Returns a list of nodes and children in a flat list for a given
420 Returns a list of nodes and children in a flat list for a given
421 path at given revision.
421 path at given revision.
422
422
423 It's possible to specify ret_type to show only `files` or `dirs`.
423 It's possible to specify ret_type to show only `files` or `dirs`.
424
424
425 This command can only be run using an |authtoken| with admin rights,
425 This command can only be run using an |authtoken| with admin rights,
426 or users with at least read rights to |repos|.
426 or users with at least read rights to |repos|.
427
427
428 :param apiuser: This is filled automatically from the |authtoken|.
428 :param apiuser: This is filled automatically from the |authtoken|.
429 :type apiuser: AuthUser
429 :type apiuser: AuthUser
430 :param repoid: The repository name or repository ID.
430 :param repoid: The repository name or repository ID.
431 :type repoid: str or int
431 :type repoid: str or int
432 :param revision: The revision for which listing should be done.
432 :param revision: The revision for which listing should be done.
433 :type revision: str
433 :type revision: str
434 :param root_path: The path from which to start displaying.
434 :param root_path: The path from which to start displaying.
435 :type root_path: str
435 :type root_path: str
436 :param ret_type: Set the return type. Valid options are
436 :param ret_type: Set the return type. Valid options are
437 ``all`` (default), ``files`` and ``dirs``.
437 ``all`` (default), ``files`` and ``dirs``.
438 :type ret_type: Optional(str)
438 :type ret_type: Optional(str)
439 :param details: Returns extended information about nodes, such as
439 :param details: Returns extended information about nodes, such as
440 md5, binary, and or content. The valid options are ``basic`` and
440 md5, binary, and or content. The valid options are ``basic`` and
441 ``full``.
441 ``full``.
442 :type details: Optional(str)
442 :type details: Optional(str)
443 :param max_file_bytes: Only return file content under this file size bytes
443 :param max_file_bytes: Only return file content under this file size bytes
444 :type details: Optional(int)
444 :type details: Optional(int)
445
445
446 Example output:
446 Example output:
447
447
448 .. code-block:: bash
448 .. code-block:: bash
449
449
450 id : <id_given_in_input>
450 id : <id_given_in_input>
451 result: [
451 result: [
452 {
452 {
453 "name" : "<name>"
453 "name" : "<name>"
454 "type" : "<type>",
454 "type" : "<type>",
455 "binary": "<true|false>" (only in extended mode)
455 "binary": "<true|false>" (only in extended mode)
456 "md5" : "<md5 of file content>" (only in extended mode)
456 "md5" : "<md5 of file content>" (only in extended mode)
457 },
457 },
458 ...
458 ...
459 ]
459 ]
460 error: null
460 error: null
461
461
462
462
463 get_repo_refs
463 get_repo_refs
464 -------------
464 -------------
465
465
466 .. py:function:: get_repo_refs(apiuser, repoid)
466 .. py:function:: get_repo_refs(apiuser, repoid)
467
467
468 Returns a dictionary of current references. It returns
468 Returns a dictionary of current references. It returns
469 bookmarks, branches, closed_branches, and tags for given repository
469 bookmarks, branches, closed_branches, and tags for given repository
470
470
471 It's possible to specify ret_type to show only `files` or `dirs`.
471 It's possible to specify ret_type to show only `files` or `dirs`.
472
472
473 This command can only be run using an |authtoken| with admin rights,
473 This command can only be run using an |authtoken| with admin rights,
474 or users with at least read rights to |repos|.
474 or users with at least read rights to |repos|.
475
475
476 :param apiuser: This is filled automatically from the |authtoken|.
476 :param apiuser: This is filled automatically from the |authtoken|.
477 :type apiuser: AuthUser
477 :type apiuser: AuthUser
478 :param repoid: The repository name or repository ID.
478 :param repoid: The repository name or repository ID.
479 :type repoid: str or int
479 :type repoid: str or int
480
480
481 Example output:
481 Example output:
482
482
483 .. code-block:: bash
483 .. code-block:: bash
484
484
485 id : <id_given_in_input>
485 id : <id_given_in_input>
486 "result": {
486 "result": {
487 "bookmarks": {
487 "bookmarks": {
488 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
488 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
489 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
489 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
490 },
490 },
491 "branches": {
491 "branches": {
492 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
492 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
493 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
493 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
494 },
494 },
495 "branches_closed": {},
495 "branches_closed": {},
496 "tags": {
496 "tags": {
497 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
497 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
498 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
498 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
499 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
499 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
500 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
500 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
501 }
501 }
502 }
502 }
503 error: null
503 error: null
504
504
505
505
506 get_repo_settings
506 get_repo_settings
507 -----------------
507 -----------------
508
508
509 .. py:function:: get_repo_settings(apiuser, repoid, key=<Optional:None>)
509 .. py:function:: get_repo_settings(apiuser, repoid, key=<Optional:None>)
510
510
511 Returns all settings for a repository. If key is given it only returns the
511 Returns all settings for a repository. If key is given it only returns the
512 setting identified by the key or null.
512 setting identified by the key or null.
513
513
514 :param apiuser: This is filled automatically from the |authtoken|.
514 :param apiuser: This is filled automatically from the |authtoken|.
515 :type apiuser: AuthUser
515 :type apiuser: AuthUser
516 :param repoid: The repository name or repository id.
516 :param repoid: The repository name or repository id.
517 :type repoid: str or int
517 :type repoid: str or int
518 :param key: Key of the setting to return.
518 :param key: Key of the setting to return.
519 :type: key: Optional(str)
519 :type: key: Optional(str)
520
520
521 Example output:
521 Example output:
522
522
523 .. code-block:: bash
523 .. code-block:: bash
524
524
525 {
525 {
526 "error": null,
526 "error": null,
527 "id": 237,
527 "id": 237,
528 "result": {
528 "result": {
529 "extensions_largefiles": true,
529 "extensions_largefiles": true,
530 "extensions_evolve": true,
530 "extensions_evolve": true,
531 "hooks_changegroup_push_logger": true,
531 "hooks_changegroup_push_logger": true,
532 "hooks_changegroup_repo_size": false,
532 "hooks_changegroup_repo_size": false,
533 "hooks_outgoing_pull_logger": true,
533 "hooks_outgoing_pull_logger": true,
534 "phases_publish": "True",
534 "phases_publish": "True",
535 "rhodecode_hg_use_rebase_for_merging": true,
535 "rhodecode_hg_use_rebase_for_merging": true,
536 "rhodecode_hg_close_branch_before_merging": false,
536 "rhodecode_hg_close_branch_before_merging": false,
537 "rhodecode_git_use_rebase_for_merging": true,
538 "rhodecode_git_close_branch_before_merging": false,
537 "rhodecode_pr_merge_enabled": true,
539 "rhodecode_pr_merge_enabled": true,
538 "rhodecode_use_outdated_comments": true
540 "rhodecode_use_outdated_comments": true
539 }
541 }
540 }
542 }
541
543
542
544
543 get_repos
545 get_repos
544 ---------
546 ---------
545
547
546 .. py:function:: get_repos(apiuser, root=<Optional:None>, traverse=<Optional:True>)
548 .. py:function:: get_repos(apiuser, root=<Optional:None>, traverse=<Optional:True>)
547
549
548 Lists all existing repositories.
550 Lists all existing repositories.
549
551
550 This command can only be run using an |authtoken| with admin rights,
552 This command can only be run using an |authtoken| with admin rights,
551 or users with at least read rights to |repos|.
553 or users with at least read rights to |repos|.
552
554
553 :param apiuser: This is filled automatically from the |authtoken|.
555 :param apiuser: This is filled automatically from the |authtoken|.
554 :type apiuser: AuthUser
556 :type apiuser: AuthUser
555 :param root: specify root repository group to fetch repositories.
557 :param root: specify root repository group to fetch repositories.
556 filters the returned repositories to be members of given root group.
558 filters the returned repositories to be members of given root group.
557 :type root: Optional(None)
559 :type root: Optional(None)
558 :param traverse: traverse given root into subrepositories. With this flag
560 :param traverse: traverse given root into subrepositories. With this flag
559 set to False, it will only return top-level repositories from `root`.
561 set to False, it will only return top-level repositories from `root`.
560 if root is empty it will return just top-level repositories.
562 if root is empty it will return just top-level repositories.
561 :type traverse: Optional(True)
563 :type traverse: Optional(True)
562
564
563
565
564 Example output:
566 Example output:
565
567
566 .. code-block:: bash
568 .. code-block:: bash
567
569
568 id : <id_given_in_input>
570 id : <id_given_in_input>
569 result: [
571 result: [
570 {
572 {
571 "repo_id" : "<repo_id>",
573 "repo_id" : "<repo_id>",
572 "repo_name" : "<reponame>"
574 "repo_name" : "<reponame>"
573 "repo_type" : "<repo_type>",
575 "repo_type" : "<repo_type>",
574 "clone_uri" : "<clone_uri>",
576 "clone_uri" : "<clone_uri>",
575 "private": : "<bool>",
577 "private": : "<bool>",
576 "created_on" : "<datetimecreated>",
578 "created_on" : "<datetimecreated>",
577 "description" : "<description>",
579 "description" : "<description>",
578 "landing_rev": "<landing_rev>",
580 "landing_rev": "<landing_rev>",
579 "owner": "<repo_owner>",
581 "owner": "<repo_owner>",
580 "fork_of": "<name_of_fork_parent>",
582 "fork_of": "<name_of_fork_parent>",
581 "enable_downloads": "<bool>",
583 "enable_downloads": "<bool>",
582 "enable_locking": "<bool>",
584 "enable_locking": "<bool>",
583 "enable_statistics": "<bool>",
585 "enable_statistics": "<bool>",
584 },
586 },
585 ...
587 ...
586 ]
588 ]
587 error: null
589 error: null
588
590
589
591
590 grant_user_group_permission
592 grant_user_group_permission
591 ---------------------------
593 ---------------------------
592
594
593 .. py:function:: grant_user_group_permission(apiuser, repoid, usergroupid, perm)
595 .. py:function:: grant_user_group_permission(apiuser, repoid, usergroupid, perm)
594
596
595 Grant permission for a user group on the specified repository,
597 Grant permission for a user group on the specified repository,
596 or update existing permissions.
598 or update existing permissions.
597
599
598 This command can only be run using an |authtoken| with admin
600 This command can only be run using an |authtoken| with admin
599 permissions on the |repo|.
601 permissions on the |repo|.
600
602
601 :param apiuser: This is filled automatically from the |authtoken|.
603 :param apiuser: This is filled automatically from the |authtoken|.
602 :type apiuser: AuthUser
604 :type apiuser: AuthUser
603 :param repoid: Set the repository name or repository ID.
605 :param repoid: Set the repository name or repository ID.
604 :type repoid: str or int
606 :type repoid: str or int
605 :param usergroupid: Specify the ID of the user group.
607 :param usergroupid: Specify the ID of the user group.
606 :type usergroupid: str or int
608 :type usergroupid: str or int
607 :param perm: Set the user group permissions using the following
609 :param perm: Set the user group permissions using the following
608 format: (repository.(none|read|write|admin))
610 format: (repository.(none|read|write|admin))
609 :type perm: str
611 :type perm: str
610
612
611 Example output:
613 Example output:
612
614
613 .. code-block:: bash
615 .. code-block:: bash
614
616
615 id : <id_given_in_input>
617 id : <id_given_in_input>
616 result : {
618 result : {
617 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
619 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
618 "success": true
620 "success": true
619
621
620 }
622 }
621 error : null
623 error : null
622
624
623 Example error output:
625 Example error output:
624
626
625 .. code-block:: bash
627 .. code-block:: bash
626
628
627 id : <id_given_in_input>
629 id : <id_given_in_input>
628 result : null
630 result : null
629 error : {
631 error : {
630 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
632 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
631 }
633 }
632
634
633
635
634 grant_user_permission
636 grant_user_permission
635 ---------------------
637 ---------------------
636
638
637 .. py:function:: grant_user_permission(apiuser, repoid, userid, perm)
639 .. py:function:: grant_user_permission(apiuser, repoid, userid, perm)
638
640
639 Grant permissions for the specified user on the given repository,
641 Grant permissions for the specified user on the given repository,
640 or update existing permissions if found.
642 or update existing permissions if found.
641
643
642 This command can only be run using an |authtoken| with admin
644 This command can only be run using an |authtoken| with admin
643 permissions on the |repo|.
645 permissions on the |repo|.
644
646
645 :param apiuser: This is filled automatically from the |authtoken|.
647 :param apiuser: This is filled automatically from the |authtoken|.
646 :type apiuser: AuthUser
648 :type apiuser: AuthUser
647 :param repoid: Set the repository name or repository ID.
649 :param repoid: Set the repository name or repository ID.
648 :type repoid: str or int
650 :type repoid: str or int
649 :param userid: Set the user name.
651 :param userid: Set the user name.
650 :type userid: str
652 :type userid: str
651 :param perm: Set the user permissions, using the following format
653 :param perm: Set the user permissions, using the following format
652 ``(repository.(none|read|write|admin))``
654 ``(repository.(none|read|write|admin))``
653 :type perm: str
655 :type perm: str
654
656
655 Example output:
657 Example output:
656
658
657 .. code-block:: bash
659 .. code-block:: bash
658
660
659 id : <id_given_in_input>
661 id : <id_given_in_input>
660 result: {
662 result: {
661 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
663 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
662 "success": true
664 "success": true
663 }
665 }
664 error: null
666 error: null
665
667
666
668
667 invalidate_cache
669 invalidate_cache
668 ----------------
670 ----------------
669
671
670 .. py:function:: invalidate_cache(apiuser, repoid, delete_keys=<Optional:False>)
672 .. py:function:: invalidate_cache(apiuser, repoid, delete_keys=<Optional:False>)
671
673
672 Invalidates the cache for the specified repository.
674 Invalidates the cache for the specified repository.
673
675
674 This command can only be run using an |authtoken| with admin rights to
676 This command can only be run using an |authtoken| with admin rights to
675 the specified repository.
677 the specified repository.
676
678
677 This command takes the following options:
679 This command takes the following options:
678
680
679 :param apiuser: This is filled automatically from |authtoken|.
681 :param apiuser: This is filled automatically from |authtoken|.
680 :type apiuser: AuthUser
682 :type apiuser: AuthUser
681 :param repoid: Sets the repository name or repository ID.
683 :param repoid: Sets the repository name or repository ID.
682 :type repoid: str or int
684 :type repoid: str or int
683 :param delete_keys: This deletes the invalidated keys instead of
685 :param delete_keys: This deletes the invalidated keys instead of
684 just flagging them.
686 just flagging them.
685 :type delete_keys: Optional(``True`` | ``False``)
687 :type delete_keys: Optional(``True`` | ``False``)
686
688
687 Example output:
689 Example output:
688
690
689 .. code-block:: bash
691 .. code-block:: bash
690
692
691 id : <id_given_in_input>
693 id : <id_given_in_input>
692 result : {
694 result : {
693 'msg': Cache for repository `<repository name>` was invalidated,
695 'msg': Cache for repository `<repository name>` was invalidated,
694 'repository': <repository name>
696 'repository': <repository name>
695 }
697 }
696 error : null
698 error : null
697
699
698 Example error output:
700 Example error output:
699
701
700 .. code-block:: bash
702 .. code-block:: bash
701
703
702 id : <id_given_in_input>
704 id : <id_given_in_input>
703 result : null
705 result : null
704 error : {
706 error : {
705 'Error occurred during cache invalidation action'
707 'Error occurred during cache invalidation action'
706 }
708 }
707
709
708
710
709 lock
711 lock
710 ----
712 ----
711
713
712 .. py:function:: lock(apiuser, repoid, locked=<Optional:None>, userid=<Optional:<OptionalAttr:apiuser>>)
714 .. py:function:: lock(apiuser, repoid, locked=<Optional:None>, userid=<Optional:<OptionalAttr:apiuser>>)
713
715
714 Sets the lock state of the specified |repo| by the given user.
716 Sets the lock state of the specified |repo| by the given user.
715 From more information, see :ref:`repo-locking`.
717 From more information, see :ref:`repo-locking`.
716
718
717 * If the ``userid`` option is not set, the repository is locked to the
719 * If the ``userid`` option is not set, the repository is locked to the
718 user who called the method.
720 user who called the method.
719 * If the ``locked`` parameter is not set, the current lock state of the
721 * If the ``locked`` parameter is not set, the current lock state of the
720 repository is displayed.
722 repository is displayed.
721
723
722 This command can only be run using an |authtoken| with admin rights to
724 This command can only be run using an |authtoken| with admin rights to
723 the specified repository.
725 the specified repository.
724
726
725 This command takes the following options:
727 This command takes the following options:
726
728
727 :param apiuser: This is filled automatically from the |authtoken|.
729 :param apiuser: This is filled automatically from the |authtoken|.
728 :type apiuser: AuthUser
730 :type apiuser: AuthUser
729 :param repoid: Sets the repository name or repository ID.
731 :param repoid: Sets the repository name or repository ID.
730 :type repoid: str or int
732 :type repoid: str or int
731 :param locked: Sets the lock state.
733 :param locked: Sets the lock state.
732 :type locked: Optional(``True`` | ``False``)
734 :type locked: Optional(``True`` | ``False``)
733 :param userid: Set the repository lock to this user.
735 :param userid: Set the repository lock to this user.
734 :type userid: Optional(str or int)
736 :type userid: Optional(str or int)
735
737
736 Example error output:
738 Example error output:
737
739
738 .. code-block:: bash
740 .. code-block:: bash
739
741
740 id : <id_given_in_input>
742 id : <id_given_in_input>
741 result : {
743 result : {
742 'repo': '<reponame>',
744 'repo': '<reponame>',
743 'locked': <bool: lock state>,
745 'locked': <bool: lock state>,
744 'locked_since': <int: lock timestamp>,
746 'locked_since': <int: lock timestamp>,
745 'locked_by': <username of person who made the lock>,
747 'locked_by': <username of person who made the lock>,
746 'lock_reason': <str: reason for locking>,
748 'lock_reason': <str: reason for locking>,
747 'lock_state_changed': <bool: True if lock state has been changed in this request>,
749 'lock_state_changed': <bool: True if lock state has been changed in this request>,
748 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
750 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
749 or
751 or
750 'msg': 'Repo `<repository name>` not locked.'
752 'msg': 'Repo `<repository name>` not locked.'
751 or
753 or
752 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
754 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
753 }
755 }
754 error : null
756 error : null
755
757
756 Example error output:
758 Example error output:
757
759
758 .. code-block:: bash
760 .. code-block:: bash
759
761
760 id : <id_given_in_input>
762 id : <id_given_in_input>
761 result : null
763 result : null
762 error : {
764 error : {
763 'Error occurred locking repository `<reponame>`'
765 'Error occurred locking repository `<reponame>`'
764 }
766 }
765
767
766
768
767 maintenance
769 maintenance
768 -----------
770 -----------
769
771
770 .. py:function:: maintenance(apiuser, repoid)
772 .. py:function:: maintenance(apiuser, repoid)
771
773
772 Triggers a maintenance on the given repository.
774 Triggers a maintenance on the given repository.
773
775
774 This command can only be run using an |authtoken| with admin
776 This command can only be run using an |authtoken| with admin
775 rights to the specified repository. For more information,
777 rights to the specified repository. For more information,
776 see :ref:`config-token-ref`.
778 see :ref:`config-token-ref`.
777
779
778 This command takes the following options:
780 This command takes the following options:
779
781
780 :param apiuser: This is filled automatically from the |authtoken|.
782 :param apiuser: This is filled automatically from the |authtoken|.
781 :type apiuser: AuthUser
783 :type apiuser: AuthUser
782 :param repoid: The repository name or repository ID.
784 :param repoid: The repository name or repository ID.
783 :type repoid: str or int
785 :type repoid: str or int
784
786
785 Example output:
787 Example output:
786
788
787 .. code-block:: bash
789 .. code-block:: bash
788
790
789 id : <id_given_in_input>
791 id : <id_given_in_input>
790 result : {
792 result : {
791 "msg": "executed maintenance command",
793 "msg": "executed maintenance command",
792 "executed_actions": [
794 "executed_actions": [
793 <action_message>, <action_message2>...
795 <action_message>, <action_message2>...
794 ],
796 ],
795 "repository": "<repository name>"
797 "repository": "<repository name>"
796 }
798 }
797 error : null
799 error : null
798
800
799 Example error output:
801 Example error output:
800
802
801 .. code-block:: bash
803 .. code-block:: bash
802
804
803 id : <id_given_in_input>
805 id : <id_given_in_input>
804 result : null
806 result : null
805 error : {
807 error : {
806 "Unable to execute maintenance on `<reponame>`"
808 "Unable to execute maintenance on `<reponame>`"
807 }
809 }
808
810
809
811
810 pull
812 pull
811 ----
813 ----
812
814
813 .. py:function:: pull(apiuser, repoid)
815 .. py:function:: pull(apiuser, repoid)
814
816
815 Triggers a pull on the given repository from a remote location. You
817 Triggers a pull on the given repository from a remote location. You
816 can use this to keep remote repositories up-to-date.
818 can use this to keep remote repositories up-to-date.
817
819
818 This command can only be run using an |authtoken| with admin
820 This command can only be run using an |authtoken| with admin
819 rights to the specified repository. For more information,
821 rights to the specified repository. For more information,
820 see :ref:`config-token-ref`.
822 see :ref:`config-token-ref`.
821
823
822 This command takes the following options:
824 This command takes the following options:
823
825
824 :param apiuser: This is filled automatically from the |authtoken|.
826 :param apiuser: This is filled automatically from the |authtoken|.
825 :type apiuser: AuthUser
827 :type apiuser: AuthUser
826 :param repoid: The repository name or repository ID.
828 :param repoid: The repository name or repository ID.
827 :type repoid: str or int
829 :type repoid: str or int
828
830
829 Example output:
831 Example output:
830
832
831 .. code-block:: bash
833 .. code-block:: bash
832
834
833 id : <id_given_in_input>
835 id : <id_given_in_input>
834 result : {
836 result : {
835 "msg": "Pulled from `<repository name>`"
837 "msg": "Pulled from `<repository name>`"
836 "repository": "<repository name>"
838 "repository": "<repository name>"
837 }
839 }
838 error : null
840 error : null
839
841
840 Example error output:
842 Example error output:
841
843
842 .. code-block:: bash
844 .. code-block:: bash
843
845
844 id : <id_given_in_input>
846 id : <id_given_in_input>
845 result : null
847 result : null
846 error : {
848 error : {
847 "Unable to pull changes from `<reponame>`"
849 "Unable to pull changes from `<reponame>`"
848 }
850 }
849
851
850
852
851 remove_field_from_repo
853 remove_field_from_repo
852 ----------------------
854 ----------------------
853
855
854 .. py:function:: remove_field_from_repo(apiuser, repoid, key)
856 .. py:function:: remove_field_from_repo(apiuser, repoid, key)
855
857
856 Removes an extra field from a repository.
858 Removes an extra field from a repository.
857
859
858 This command can only be run using an |authtoken| with at least
860 This command can only be run using an |authtoken| with at least
859 write permissions to the |repo|.
861 write permissions to the |repo|.
860
862
861 :param apiuser: This is filled automatically from the |authtoken|.
863 :param apiuser: This is filled automatically from the |authtoken|.
862 :type apiuser: AuthUser
864 :type apiuser: AuthUser
863 :param repoid: Set the repository name or repository ID.
865 :param repoid: Set the repository name or repository ID.
864 :type repoid: str or int
866 :type repoid: str or int
865 :param key: Set the unique field key for this repository.
867 :param key: Set the unique field key for this repository.
866 :type key: str
868 :type key: str
867
869
868
870
869 revoke_user_group_permission
871 revoke_user_group_permission
870 ----------------------------
872 ----------------------------
871
873
872 .. py:function:: revoke_user_group_permission(apiuser, repoid, usergroupid)
874 .. py:function:: revoke_user_group_permission(apiuser, repoid, usergroupid)
873
875
874 Revoke the permissions of a user group on a given repository.
876 Revoke the permissions of a user group on a given repository.
875
877
876 This command can only be run using an |authtoken| with admin
878 This command can only be run using an |authtoken| with admin
877 permissions on the |repo|.
879 permissions on the |repo|.
878
880
879 :param apiuser: This is filled automatically from the |authtoken|.
881 :param apiuser: This is filled automatically from the |authtoken|.
880 :type apiuser: AuthUser
882 :type apiuser: AuthUser
881 :param repoid: Set the repository name or repository ID.
883 :param repoid: Set the repository name or repository ID.
882 :type repoid: str or int
884 :type repoid: str or int
883 :param usergroupid: Specify the user group ID.
885 :param usergroupid: Specify the user group ID.
884 :type usergroupid: str or int
886 :type usergroupid: str or int
885
887
886 Example output:
888 Example output:
887
889
888 .. code-block:: bash
890 .. code-block:: bash
889
891
890 id : <id_given_in_input>
892 id : <id_given_in_input>
891 result: {
893 result: {
892 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
894 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
893 "success": true
895 "success": true
894 }
896 }
895 error: null
897 error: null
896
898
897
899
898 revoke_user_permission
900 revoke_user_permission
899 ----------------------
901 ----------------------
900
902
901 .. py:function:: revoke_user_permission(apiuser, repoid, userid)
903 .. py:function:: revoke_user_permission(apiuser, repoid, userid)
902
904
903 Revoke permission for a user on the specified repository.
905 Revoke permission for a user on the specified repository.
904
906
905 This command can only be run using an |authtoken| with admin
907 This command can only be run using an |authtoken| with admin
906 permissions on the |repo|.
908 permissions on the |repo|.
907
909
908 :param apiuser: This is filled automatically from the |authtoken|.
910 :param apiuser: This is filled automatically from the |authtoken|.
909 :type apiuser: AuthUser
911 :type apiuser: AuthUser
910 :param repoid: Set the repository name or repository ID.
912 :param repoid: Set the repository name or repository ID.
911 :type repoid: str or int
913 :type repoid: str or int
912 :param userid: Set the user name of revoked user.
914 :param userid: Set the user name of revoked user.
913 :type userid: str or int
915 :type userid: str or int
914
916
915 Example error output:
917 Example error output:
916
918
917 .. code-block:: bash
919 .. code-block:: bash
918
920
919 id : <id_given_in_input>
921 id : <id_given_in_input>
920 result: {
922 result: {
921 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
923 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
922 "success": true
924 "success": true
923 }
925 }
924 error: null
926 error: null
925
927
926
928
927 set_repo_settings
929 set_repo_settings
928 -----------------
930 -----------------
929
931
930 .. py:function:: set_repo_settings(apiuser, repoid, settings)
932 .. py:function:: set_repo_settings(apiuser, repoid, settings)
931
933
932 Update repository settings. Returns true on success.
934 Update repository settings. Returns true on success.
933
935
934 :param apiuser: This is filled automatically from the |authtoken|.
936 :param apiuser: This is filled automatically from the |authtoken|.
935 :type apiuser: AuthUser
937 :type apiuser: AuthUser
936 :param repoid: The repository name or repository id.
938 :param repoid: The repository name or repository id.
937 :type repoid: str or int
939 :type repoid: str or int
938 :param settings: The new settings for the repository.
940 :param settings: The new settings for the repository.
939 :type: settings: dict
941 :type: settings: dict
940
942
941 Example output:
943 Example output:
942
944
943 .. code-block:: bash
945 .. code-block:: bash
944
946
945 {
947 {
946 "error": null,
948 "error": null,
947 "id": 237,
949 "id": 237,
948 "result": true
950 "result": true
949 }
951 }
950
952
951
953
952 strip
954 strip
953 -----
955 -----
954
956
955 .. py:function:: strip(apiuser, repoid, revision, branch)
957 .. py:function:: strip(apiuser, repoid, revision, branch)
956
958
957 Strips the given revision from the specified repository.
959 Strips the given revision from the specified repository.
958
960
959 * This will remove the revision and all of its decendants.
961 * This will remove the revision and all of its decendants.
960
962
961 This command can only be run using an |authtoken| with admin rights to
963 This command can only be run using an |authtoken| with admin rights to
962 the specified repository.
964 the specified repository.
963
965
964 This command takes the following options:
966 This command takes the following options:
965
967
966 :param apiuser: This is filled automatically from the |authtoken|.
968 :param apiuser: This is filled automatically from the |authtoken|.
967 :type apiuser: AuthUser
969 :type apiuser: AuthUser
968 :param repoid: The repository name or repository ID.
970 :param repoid: The repository name or repository ID.
969 :type repoid: str or int
971 :type repoid: str or int
970 :param revision: The revision you wish to strip.
972 :param revision: The revision you wish to strip.
971 :type revision: str
973 :type revision: str
972 :param branch: The branch from which to strip the revision.
974 :param branch: The branch from which to strip the revision.
973 :type branch: str
975 :type branch: str
974
976
975 Example output:
977 Example output:
976
978
977 .. code-block:: bash
979 .. code-block:: bash
978
980
979 id : <id_given_in_input>
981 id : <id_given_in_input>
980 result : {
982 result : {
981 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
983 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
982 "repository": "<repository name>"
984 "repository": "<repository name>"
983 }
985 }
984 error : null
986 error : null
985
987
986 Example error output:
988 Example error output:
987
989
988 .. code-block:: bash
990 .. code-block:: bash
989
991
990 id : <id_given_in_input>
992 id : <id_given_in_input>
991 result : null
993 result : null
992 error : {
994 error : {
993 "Unable to strip commit <commit_hash> from repo `<repository name>`"
995 "Unable to strip commit <commit_hash> from repo `<repository name>`"
994 }
996 }
995
997
996
998
997 update_repo
999 update_repo
998 -----------
1000 -----------
999
1001
1000 .. py:function:: update_repo(apiuser, repoid, repo_name=<Optional:None>, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, fork_of=<Optional:None>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, fields=<Optional:''>)
1002 .. py:function:: update_repo(apiuser, repoid, repo_name=<Optional:None>, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, fork_of=<Optional:None>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, fields=<Optional:''>)
1001
1003
1002 Updates a repository with the given information.
1004 Updates a repository with the given information.
1003
1005
1004 This command can only be run using an |authtoken| with at least
1006 This command can only be run using an |authtoken| with at least
1005 admin permissions to the |repo|.
1007 admin permissions to the |repo|.
1006
1008
1007 * If the repository name contains "/", repository will be updated
1009 * If the repository name contains "/", repository will be updated
1008 accordingly with a repository group or nested repository groups
1010 accordingly with a repository group or nested repository groups
1009
1011
1010 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
1012 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
1011 called "repo-test" and place it inside group "foo/bar".
1013 called "repo-test" and place it inside group "foo/bar".
1012 You have to have permissions to access and write to the last repository
1014 You have to have permissions to access and write to the last repository
1013 group ("bar" in this example)
1015 group ("bar" in this example)
1014
1016
1015 :param apiuser: This is filled automatically from the |authtoken|.
1017 :param apiuser: This is filled automatically from the |authtoken|.
1016 :type apiuser: AuthUser
1018 :type apiuser: AuthUser
1017 :param repoid: repository name or repository ID.
1019 :param repoid: repository name or repository ID.
1018 :type repoid: str or int
1020 :type repoid: str or int
1019 :param repo_name: Update the |repo| name, including the
1021 :param repo_name: Update the |repo| name, including the
1020 repository group it's in.
1022 repository group it's in.
1021 :type repo_name: str
1023 :type repo_name: str
1022 :param owner: Set the |repo| owner.
1024 :param owner: Set the |repo| owner.
1023 :type owner: str
1025 :type owner: str
1024 :param fork_of: Set the |repo| as fork of another |repo|.
1026 :param fork_of: Set the |repo| as fork of another |repo|.
1025 :type fork_of: str
1027 :type fork_of: str
1026 :param description: Update the |repo| description.
1028 :param description: Update the |repo| description.
1027 :type description: str
1029 :type description: str
1028 :param private: Set the |repo| as private. (True | False)
1030 :param private: Set the |repo| as private. (True | False)
1029 :type private: bool
1031 :type private: bool
1030 :param clone_uri: Update the |repo| clone URI.
1032 :param clone_uri: Update the |repo| clone URI.
1031 :type clone_uri: str
1033 :type clone_uri: str
1032 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
1034 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
1033 :type landing_rev: str
1035 :type landing_rev: str
1034 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1036 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1035 :type enable_statistics: bool
1037 :type enable_statistics: bool
1036 :param enable_locking: Enable |repo| locking.
1038 :param enable_locking: Enable |repo| locking.
1037 :type enable_locking: bool
1039 :type enable_locking: bool
1038 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1040 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1039 :type enable_downloads: bool
1041 :type enable_downloads: bool
1040 :param fields: Add extra fields to the |repo|. Use the following
1042 :param fields: Add extra fields to the |repo|. Use the following
1041 example format: ``field_key=field_val,field_key2=fieldval2``.
1043 example format: ``field_key=field_val,field_key2=fieldval2``.
1042 Escape ', ' with \,
1044 Escape ', ' with \,
1043 :type fields: str
1045 :type fields: str
1044
1046
1045
1047
@@ -1,855 +1,875 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 HG repository module
22 HG repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import binascii
26 import binascii
27 import os
27 import os
28 import shutil
28 import shutil
29 import urllib
29 import urllib
30
30
31 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32
32
33 from rhodecode.lib.compat import OrderedDict
33 from rhodecode.lib.compat import OrderedDict
34 from rhodecode.lib.datelib import (
34 from rhodecode.lib.datelib import (
35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
36 date_astimestamp)
36 date_astimestamp)
37 from rhodecode.lib.utils import safe_unicode, safe_str
37 from rhodecode.lib.utils import safe_unicode, safe_str
38 from rhodecode.lib.vcs import connection
38 from rhodecode.lib.vcs import connection
39 from rhodecode.lib.vcs.backends.base import (
39 from rhodecode.lib.vcs.backends.base import (
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
41 MergeFailureReason, Reference)
41 MergeFailureReason, Reference)
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
48
48
49 hexlify = binascii.hexlify
49 hexlify = binascii.hexlify
50 nullid = "\0" * 20
50 nullid = "\0" * 20
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MercurialRepository(BaseRepository):
55 class MercurialRepository(BaseRepository):
56 """
56 """
57 Mercurial repository backend
57 Mercurial repository backend
58 """
58 """
59 DEFAULT_BRANCH_NAME = 'default'
59 DEFAULT_BRANCH_NAME = 'default'
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 update_after_clone=False, with_wire=None):
62 update_after_clone=False, with_wire=None):
63 """
63 """
64 Raises RepositoryError if repository could not be find at the given
64 Raises RepositoryError if repository could not be find at the given
65 ``repo_path``.
65 ``repo_path``.
66
66
67 :param repo_path: local path of the repository
67 :param repo_path: local path of the repository
68 :param config: config object containing the repo configuration
68 :param config: config object containing the repo configuration
69 :param create=False: if set to True, would try to create repository if
69 :param create=False: if set to True, would try to create repository if
70 it does not exist rather than raising exception
70 it does not exist rather than raising exception
71 :param src_url=None: would try to clone repository from given location
71 :param src_url=None: would try to clone repository from given location
72 :param update_after_clone=False: sets update of working copy after
72 :param update_after_clone=False: sets update of working copy after
73 making a clone
73 making a clone
74 """
74 """
75 self.path = safe_str(os.path.abspath(repo_path))
75 self.path = safe_str(os.path.abspath(repo_path))
76 self.config = config if config else Config()
76 self.config = config if config else Config()
77 self._remote = connection.Hg(
77 self._remote = connection.Hg(
78 self.path, self.config, with_wire=with_wire)
78 self.path, self.config, with_wire=with_wire)
79
79
80 self._init_repo(create, src_url, update_after_clone)
80 self._init_repo(create, src_url, update_after_clone)
81
81
82 # caches
82 # caches
83 self._commit_ids = {}
83 self._commit_ids = {}
84
84
85 @LazyProperty
85 @LazyProperty
86 def commit_ids(self):
86 def commit_ids(self):
87 """
87 """
88 Returns list of commit ids, in ascending order. Being lazy
88 Returns list of commit ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
89 attribute allows external tools to inject shas from cache.
90 """
90 """
91 commit_ids = self._get_all_commit_ids()
91 commit_ids = self._get_all_commit_ids()
92 self._rebuild_cache(commit_ids)
92 self._rebuild_cache(commit_ids)
93 return commit_ids
93 return commit_ids
94
94
95 def _rebuild_cache(self, commit_ids):
95 def _rebuild_cache(self, commit_ids):
96 self._commit_ids = dict((commit_id, index)
96 self._commit_ids = dict((commit_id, index)
97 for index, commit_id in enumerate(commit_ids))
97 for index, commit_id in enumerate(commit_ids))
98
98
99 @LazyProperty
99 @LazyProperty
100 def branches(self):
100 def branches(self):
101 return self._get_branches()
101 return self._get_branches()
102
102
103 @LazyProperty
103 @LazyProperty
104 def branches_closed(self):
104 def branches_closed(self):
105 return self._get_branches(active=False, closed=True)
105 return self._get_branches(active=False, closed=True)
106
106
107 @LazyProperty
107 @LazyProperty
108 def branches_all(self):
108 def branches_all(self):
109 all_branches = {}
109 all_branches = {}
110 all_branches.update(self.branches)
110 all_branches.update(self.branches)
111 all_branches.update(self.branches_closed)
111 all_branches.update(self.branches_closed)
112 return all_branches
112 return all_branches
113
113
114 def _get_branches(self, active=True, closed=False):
114 def _get_branches(self, active=True, closed=False):
115 """
115 """
116 Gets branches for this repository
116 Gets branches for this repository
117 Returns only not closed active branches by default
117 Returns only not closed active branches by default
118
118
119 :param active: return also active branches
119 :param active: return also active branches
120 :param closed: return also closed branches
120 :param closed: return also closed branches
121
121
122 """
122 """
123 if self.is_empty():
123 if self.is_empty():
124 return {}
124 return {}
125
125
126 def get_name(ctx):
126 def get_name(ctx):
127 return ctx[0]
127 return ctx[0]
128
128
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
130 self._remote.branches(active, closed).items()]
130 self._remote.branches(active, closed).items()]
131
131
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
133
133
134 @LazyProperty
134 @LazyProperty
135 def tags(self):
135 def tags(self):
136 """
136 """
137 Gets tags for this repository
137 Gets tags for this repository
138 """
138 """
139 return self._get_tags()
139 return self._get_tags()
140
140
141 def _get_tags(self):
141 def _get_tags(self):
142 if self.is_empty():
142 if self.is_empty():
143 return {}
143 return {}
144
144
145 def get_name(ctx):
145 def get_name(ctx):
146 return ctx[0]
146 return ctx[0]
147
147
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
149 self._remote.tags().items()]
149 self._remote.tags().items()]
150
150
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
152
152
153 def tag(self, name, user, commit_id=None, message=None, date=None,
153 def tag(self, name, user, commit_id=None, message=None, date=None,
154 **kwargs):
154 **kwargs):
155 """
155 """
156 Creates and returns a tag for the given ``commit_id``.
156 Creates and returns a tag for the given ``commit_id``.
157
157
158 :param name: name for new tag
158 :param name: name for new tag
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
160 :param commit_id: commit id for which new tag would be created
160 :param commit_id: commit id for which new tag would be created
161 :param message: message of the tag's commit
161 :param message: message of the tag's commit
162 :param date: date of tag's commit
162 :param date: date of tag's commit
163
163
164 :raises TagAlreadyExistError: if tag with same name already exists
164 :raises TagAlreadyExistError: if tag with same name already exists
165 """
165 """
166 if name in self.tags:
166 if name in self.tags:
167 raise TagAlreadyExistError("Tag %s already exists" % name)
167 raise TagAlreadyExistError("Tag %s already exists" % name)
168 commit = self.get_commit(commit_id=commit_id)
168 commit = self.get_commit(commit_id=commit_id)
169 local = kwargs.setdefault('local', False)
169 local = kwargs.setdefault('local', False)
170
170
171 if message is None:
171 if message is None:
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
173
173
174 date, tz = date_to_timestamp_plus_offset(date)
174 date, tz = date_to_timestamp_plus_offset(date)
175
175
176 self._remote.tag(
176 self._remote.tag(
177 name, commit.raw_id, message, local, user, date, tz)
177 name, commit.raw_id, message, local, user, date, tz)
178 self._remote.invalidate_vcs_cache()
178 self._remote.invalidate_vcs_cache()
179
179
180 # Reinitialize tags
180 # Reinitialize tags
181 self.tags = self._get_tags()
181 self.tags = self._get_tags()
182 tag_id = self.tags[name]
182 tag_id = self.tags[name]
183
183
184 return self.get_commit(commit_id=tag_id)
184 return self.get_commit(commit_id=tag_id)
185
185
186 def remove_tag(self, name, user, message=None, date=None):
186 def remove_tag(self, name, user, message=None, date=None):
187 """
187 """
188 Removes tag with the given `name`.
188 Removes tag with the given `name`.
189
189
190 :param name: name of the tag to be removed
190 :param name: name of the tag to be removed
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
192 :param message: message of the tag's removal commit
192 :param message: message of the tag's removal commit
193 :param date: date of tag's removal commit
193 :param date: date of tag's removal commit
194
194
195 :raises TagDoesNotExistError: if tag with given name does not exists
195 :raises TagDoesNotExistError: if tag with given name does not exists
196 """
196 """
197 if name not in self.tags:
197 if name not in self.tags:
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
199 if message is None:
199 if message is None:
200 message = "Removed tag %s" % name
200 message = "Removed tag %s" % name
201 local = False
201 local = False
202
202
203 date, tz = date_to_timestamp_plus_offset(date)
203 date, tz = date_to_timestamp_plus_offset(date)
204
204
205 self._remote.tag(name, nullid, message, local, user, date, tz)
205 self._remote.tag(name, nullid, message, local, user, date, tz)
206 self._remote.invalidate_vcs_cache()
206 self._remote.invalidate_vcs_cache()
207 self.tags = self._get_tags()
207 self.tags = self._get_tags()
208
208
209 @LazyProperty
209 @LazyProperty
210 def bookmarks(self):
210 def bookmarks(self):
211 """
211 """
212 Gets bookmarks for this repository
212 Gets bookmarks for this repository
213 """
213 """
214 return self._get_bookmarks()
214 return self._get_bookmarks()
215
215
216 def _get_bookmarks(self):
216 def _get_bookmarks(self):
217 if self.is_empty():
217 if self.is_empty():
218 return {}
218 return {}
219
219
220 def get_name(ctx):
220 def get_name(ctx):
221 return ctx[0]
221 return ctx[0]
222
222
223 _bookmarks = [
223 _bookmarks = [
224 (safe_unicode(n), hexlify(h)) for n, h in
224 (safe_unicode(n), hexlify(h)) for n, h in
225 self._remote.bookmarks().items()]
225 self._remote.bookmarks().items()]
226
226
227 return OrderedDict(sorted(_bookmarks, key=get_name))
227 return OrderedDict(sorted(_bookmarks, key=get_name))
228
228
229 def _get_all_commit_ids(self):
229 def _get_all_commit_ids(self):
230 return self._remote.get_all_commit_ids('visible')
230 return self._remote.get_all_commit_ids('visible')
231
231
232 def get_diff(
232 def get_diff(
233 self, commit1, commit2, path='', ignore_whitespace=False,
233 self, commit1, commit2, path='', ignore_whitespace=False,
234 context=3, path1=None):
234 context=3, path1=None):
235 """
235 """
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
237 `commit2` since `commit1`.
237 `commit2` since `commit1`.
238
238
239 :param commit1: Entry point from which diff is shown. Can be
239 :param commit1: Entry point from which diff is shown. Can be
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
241 the changes since empty state of the repository until `commit2`
241 the changes since empty state of the repository until `commit2`
242 :param commit2: Until which commit changes should be shown.
242 :param commit2: Until which commit changes should be shown.
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
244 changes. Defaults to ``False``.
244 changes. Defaults to ``False``.
245 :param context: How many lines before/after changed lines should be
245 :param context: How many lines before/after changed lines should be
246 shown. Defaults to ``3``.
246 shown. Defaults to ``3``.
247 """
247 """
248 self._validate_diff_commits(commit1, commit2)
248 self._validate_diff_commits(commit1, commit2)
249 if path1 is not None and path1 != path:
249 if path1 is not None and path1 != path:
250 raise ValueError("Diff of two different paths not supported.")
250 raise ValueError("Diff of two different paths not supported.")
251
251
252 if path:
252 if path:
253 file_filter = [self.path, path]
253 file_filter = [self.path, path]
254 else:
254 else:
255 file_filter = None
255 file_filter = None
256
256
257 diff = self._remote.diff(
257 diff = self._remote.diff(
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
259 opt_git=True, opt_ignorews=ignore_whitespace,
259 opt_git=True, opt_ignorews=ignore_whitespace,
260 context=context)
260 context=context)
261 return MercurialDiff(diff)
261 return MercurialDiff(diff)
262
262
263 def strip(self, commit_id, branch=None):
263 def strip(self, commit_id, branch=None):
264 self._remote.strip(commit_id, update=False, backup="none")
264 self._remote.strip(commit_id, update=False, backup="none")
265
265
266 self._remote.invalidate_vcs_cache()
266 self._remote.invalidate_vcs_cache()
267 self.commit_ids = self._get_all_commit_ids()
267 self.commit_ids = self._get_all_commit_ids()
268 self._rebuild_cache(self.commit_ids)
268 self._rebuild_cache(self.commit_ids)
269
269
270 def verify(self):
270 def verify(self):
271 verify = self._remote.verify()
271 verify = self._remote.verify()
272
272
273 self._remote.invalidate_vcs_cache()
273 self._remote.invalidate_vcs_cache()
274 return verify
274 return verify
275
275
276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
277 if commit_id1 == commit_id2:
277 if commit_id1 == commit_id2:
278 return commit_id1
278 return commit_id1
279
279
280 ancestors = self._remote.revs_from_revspec(
280 ancestors = self._remote.revs_from_revspec(
281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
282 other_path=repo2.path)
282 other_path=repo2.path)
283 return repo2[ancestors[0]].raw_id if ancestors else None
283 return repo2[ancestors[0]].raw_id if ancestors else None
284
284
285 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
285 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
286 if commit_id1 == commit_id2:
286 if commit_id1 == commit_id2:
287 commits = []
287 commits = []
288 else:
288 else:
289 if merge:
289 if merge:
290 indexes = self._remote.revs_from_revspec(
290 indexes = self._remote.revs_from_revspec(
291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
293 else:
293 else:
294 indexes = self._remote.revs_from_revspec(
294 indexes = self._remote.revs_from_revspec(
295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
296 commit_id1, other_path=repo2.path)
296 commit_id1, other_path=repo2.path)
297
297
298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
299 for idx in indexes]
299 for idx in indexes]
300
300
301 return commits
301 return commits
302
302
303 @staticmethod
303 @staticmethod
304 def check_url(url, config):
304 def check_url(url, config):
305 """
305 """
306 Function will check given url and try to verify if it's a valid
306 Function will check given url and try to verify if it's a valid
307 link. Sometimes it may happened that mercurial will issue basic
307 link. Sometimes it may happened that mercurial will issue basic
308 auth request that can cause whole API to hang when used from python
308 auth request that can cause whole API to hang when used from python
309 or other external calls.
309 or other external calls.
310
310
311 On failures it'll raise urllib2.HTTPError, exception is also thrown
311 On failures it'll raise urllib2.HTTPError, exception is also thrown
312 when the return code is non 200
312 when the return code is non 200
313 """
313 """
314 # check first if it's not an local url
314 # check first if it's not an local url
315 if os.path.isdir(url) or url.startswith('file:'):
315 if os.path.isdir(url) or url.startswith('file:'):
316 return True
316 return True
317
317
318 # Request the _remote to verify the url
318 # Request the _remote to verify the url
319 return connection.Hg.check_url(url, config.serialize())
319 return connection.Hg.check_url(url, config.serialize())
320
320
321 @staticmethod
321 @staticmethod
322 def is_valid_repository(path):
322 def is_valid_repository(path):
323 return os.path.isdir(os.path.join(path, '.hg'))
323 return os.path.isdir(os.path.join(path, '.hg'))
324
324
325 def _init_repo(self, create, src_url=None, update_after_clone=False):
325 def _init_repo(self, create, src_url=None, update_after_clone=False):
326 """
326 """
327 Function will check for mercurial repository in given path. If there
327 Function will check for mercurial repository in given path. If there
328 is no repository in that path it will raise an exception unless
328 is no repository in that path it will raise an exception unless
329 `create` parameter is set to True - in that case repository would
329 `create` parameter is set to True - in that case repository would
330 be created.
330 be created.
331
331
332 If `src_url` is given, would try to clone repository from the
332 If `src_url` is given, would try to clone repository from the
333 location at given clone_point. Additionally it'll make update to
333 location at given clone_point. Additionally it'll make update to
334 working copy accordingly to `update_after_clone` flag.
334 working copy accordingly to `update_after_clone` flag.
335 """
335 """
336 if create and os.path.exists(self.path):
336 if create and os.path.exists(self.path):
337 raise RepositoryError(
337 raise RepositoryError(
338 "Cannot create repository at %s, location already exist"
338 "Cannot create repository at %s, location already exist"
339 % self.path)
339 % self.path)
340
340
341 if src_url:
341 if src_url:
342 url = str(self._get_url(src_url))
342 url = str(self._get_url(src_url))
343 MercurialRepository.check_url(url, self.config)
343 MercurialRepository.check_url(url, self.config)
344
344
345 self._remote.clone(url, self.path, update_after_clone)
345 self._remote.clone(url, self.path, update_after_clone)
346
346
347 # Don't try to create if we've already cloned repo
347 # Don't try to create if we've already cloned repo
348 create = False
348 create = False
349
349
350 if create:
350 if create:
351 os.makedirs(self.path, mode=0755)
351 os.makedirs(self.path, mode=0755)
352
352
353 self._remote.localrepository(create)
353 self._remote.localrepository(create)
354
354
355 @LazyProperty
355 @LazyProperty
356 def in_memory_commit(self):
356 def in_memory_commit(self):
357 return MercurialInMemoryCommit(self)
357 return MercurialInMemoryCommit(self)
358
358
359 @LazyProperty
359 @LazyProperty
360 def description(self):
360 def description(self):
361 description = self._remote.get_config_value(
361 description = self._remote.get_config_value(
362 'web', 'description', untrusted=True)
362 'web', 'description', untrusted=True)
363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
364
364
365 @LazyProperty
365 @LazyProperty
366 def contact(self):
366 def contact(self):
367 contact = (
367 contact = (
368 self._remote.get_config_value("web", "contact") or
368 self._remote.get_config_value("web", "contact") or
369 self._remote.get_config_value("ui", "username"))
369 self._remote.get_config_value("ui", "username"))
370 return safe_unicode(contact or self.DEFAULT_CONTACT)
370 return safe_unicode(contact or self.DEFAULT_CONTACT)
371
371
372 @LazyProperty
372 @LazyProperty
373 def last_change(self):
373 def last_change(self):
374 """
374 """
375 Returns last change made on this repository as
375 Returns last change made on this repository as
376 `datetime.datetime` object.
376 `datetime.datetime` object.
377 """
377 """
378 try:
378 try:
379 return self.get_commit().date
379 return self.get_commit().date
380 except RepositoryError:
380 except RepositoryError:
381 tzoffset = makedate()[1]
381 tzoffset = makedate()[1]
382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
383
383
384 def _get_fs_mtime(self):
384 def _get_fs_mtime(self):
385 # fallback to filesystem
385 # fallback to filesystem
386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
387 st_path = os.path.join(self.path, '.hg', "store")
387 st_path = os.path.join(self.path, '.hg', "store")
388 if os.path.exists(cl_path):
388 if os.path.exists(cl_path):
389 return os.stat(cl_path).st_mtime
389 return os.stat(cl_path).st_mtime
390 else:
390 else:
391 return os.stat(st_path).st_mtime
391 return os.stat(st_path).st_mtime
392
392
393 def _sanitize_commit_idx(self, idx):
393 def _sanitize_commit_idx(self, idx):
394 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
394 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
395 # number. A `long` is treated in the correct way though. So we convert
395 # number. A `long` is treated in the correct way though. So we convert
396 # `int` to `long` here to make sure it is handled correctly.
396 # `int` to `long` here to make sure it is handled correctly.
397 if isinstance(idx, int):
397 if isinstance(idx, int):
398 return long(idx)
398 return long(idx)
399 return idx
399 return idx
400
400
401 def _get_url(self, url):
401 def _get_url(self, url):
402 """
402 """
403 Returns normalized url. If schema is not given, would fall
403 Returns normalized url. If schema is not given, would fall
404 to filesystem
404 to filesystem
405 (``file:///``) schema.
405 (``file:///``) schema.
406 """
406 """
407 url = url.encode('utf8')
407 url = url.encode('utf8')
408 if url != 'default' and '://' not in url:
408 if url != 'default' and '://' not in url:
409 url = "file:" + urllib.pathname2url(url)
409 url = "file:" + urllib.pathname2url(url)
410 return url
410 return url
411
411
412 def get_hook_location(self):
412 def get_hook_location(self):
413 """
413 """
414 returns absolute path to location where hooks are stored
414 returns absolute path to location where hooks are stored
415 """
415 """
416 return os.path.join(self.path, '.hg', '.hgrc')
416 return os.path.join(self.path, '.hg', '.hgrc')
417
417
418 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
418 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
419 """
419 """
420 Returns ``MercurialCommit`` object representing repository's
420 Returns ``MercurialCommit`` object representing repository's
421 commit at the given `commit_id` or `commit_idx`.
421 commit at the given `commit_id` or `commit_idx`.
422 """
422 """
423 if self.is_empty():
423 if self.is_empty():
424 raise EmptyRepositoryError("There are no commits yet")
424 raise EmptyRepositoryError("There are no commits yet")
425
425
426 if commit_id is not None:
426 if commit_id is not None:
427 self._validate_commit_id(commit_id)
427 self._validate_commit_id(commit_id)
428 try:
428 try:
429 idx = self._commit_ids[commit_id]
429 idx = self._commit_ids[commit_id]
430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
431 except KeyError:
431 except KeyError:
432 pass
432 pass
433 elif commit_idx is not None:
433 elif commit_idx is not None:
434 self._validate_commit_idx(commit_idx)
434 self._validate_commit_idx(commit_idx)
435 commit_idx = self._sanitize_commit_idx(commit_idx)
435 commit_idx = self._sanitize_commit_idx(commit_idx)
436 try:
436 try:
437 id_ = self.commit_ids[commit_idx]
437 id_ = self.commit_ids[commit_idx]
438 if commit_idx < 0:
438 if commit_idx < 0:
439 commit_idx += len(self.commit_ids)
439 commit_idx += len(self.commit_ids)
440 return MercurialCommit(
440 return MercurialCommit(
441 self, id_, commit_idx, pre_load=pre_load)
441 self, id_, commit_idx, pre_load=pre_load)
442 except IndexError:
442 except IndexError:
443 commit_id = commit_idx
443 commit_id = commit_idx
444 else:
444 else:
445 commit_id = "tip"
445 commit_id = "tip"
446
446
447 # TODO Paris: Ugly hack to "serialize" long for msgpack
447 # TODO Paris: Ugly hack to "serialize" long for msgpack
448 if isinstance(commit_id, long):
448 if isinstance(commit_id, long):
449 commit_id = float(commit_id)
449 commit_id = float(commit_id)
450
450
451 if isinstance(commit_id, unicode):
451 if isinstance(commit_id, unicode):
452 commit_id = safe_str(commit_id)
452 commit_id = safe_str(commit_id)
453
453
454 try:
454 try:
455 raw_id, idx = self._remote.lookup(commit_id, both=True)
455 raw_id, idx = self._remote.lookup(commit_id, both=True)
456 except CommitDoesNotExistError:
456 except CommitDoesNotExistError:
457 msg = "Commit %s does not exist for %s" % (
457 msg = "Commit %s does not exist for %s" % (
458 commit_id, self)
458 commit_id, self)
459 raise CommitDoesNotExistError(msg)
459 raise CommitDoesNotExistError(msg)
460
460
461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
462
462
463 def get_commits(
463 def get_commits(
464 self, start_id=None, end_id=None, start_date=None, end_date=None,
464 self, start_id=None, end_id=None, start_date=None, end_date=None,
465 branch_name=None, pre_load=None):
465 branch_name=None, pre_load=None):
466 """
466 """
467 Returns generator of ``MercurialCommit`` objects from start to end
467 Returns generator of ``MercurialCommit`` objects from start to end
468 (both are inclusive)
468 (both are inclusive)
469
469
470 :param start_id: None, str(commit_id)
470 :param start_id: None, str(commit_id)
471 :param end_id: None, str(commit_id)
471 :param end_id: None, str(commit_id)
472 :param start_date: if specified, commits with commit date less than
472 :param start_date: if specified, commits with commit date less than
473 ``start_date`` would be filtered out from returned set
473 ``start_date`` would be filtered out from returned set
474 :param end_date: if specified, commits with commit date greater than
474 :param end_date: if specified, commits with commit date greater than
475 ``end_date`` would be filtered out from returned set
475 ``end_date`` would be filtered out from returned set
476 :param branch_name: if specified, commits not reachable from given
476 :param branch_name: if specified, commits not reachable from given
477 branch would be filtered out from returned set
477 branch would be filtered out from returned set
478
478
479 :raise BranchDoesNotExistError: If given ``branch_name`` does not
479 :raise BranchDoesNotExistError: If given ``branch_name`` does not
480 exist.
480 exist.
481 :raise CommitDoesNotExistError: If commit for given ``start`` or
481 :raise CommitDoesNotExistError: If commit for given ``start`` or
482 ``end`` could not be found.
482 ``end`` could not be found.
483 """
483 """
484 # actually we should check now if it's not an empty repo
484 # actually we should check now if it's not an empty repo
485 branch_ancestors = False
485 branch_ancestors = False
486 if self.is_empty():
486 if self.is_empty():
487 raise EmptyRepositoryError("There are no commits yet")
487 raise EmptyRepositoryError("There are no commits yet")
488 self._validate_branch_name(branch_name)
488 self._validate_branch_name(branch_name)
489
489
490 if start_id is not None:
490 if start_id is not None:
491 self._validate_commit_id(start_id)
491 self._validate_commit_id(start_id)
492 c_start = self.get_commit(commit_id=start_id)
492 c_start = self.get_commit(commit_id=start_id)
493 start_pos = self._commit_ids[c_start.raw_id]
493 start_pos = self._commit_ids[c_start.raw_id]
494 else:
494 else:
495 start_pos = None
495 start_pos = None
496
496
497 if end_id is not None:
497 if end_id is not None:
498 self._validate_commit_id(end_id)
498 self._validate_commit_id(end_id)
499 c_end = self.get_commit(commit_id=end_id)
499 c_end = self.get_commit(commit_id=end_id)
500 end_pos = max(0, self._commit_ids[c_end.raw_id])
500 end_pos = max(0, self._commit_ids[c_end.raw_id])
501 else:
501 else:
502 end_pos = None
502 end_pos = None
503
503
504 if None not in [start_id, end_id] and start_pos > end_pos:
504 if None not in [start_id, end_id] and start_pos > end_pos:
505 raise RepositoryError(
505 raise RepositoryError(
506 "Start commit '%s' cannot be after end commit '%s'" %
506 "Start commit '%s' cannot be after end commit '%s'" %
507 (start_id, end_id))
507 (start_id, end_id))
508
508
509 if end_pos is not None:
509 if end_pos is not None:
510 end_pos += 1
510 end_pos += 1
511
511
512 commit_filter = []
512 commit_filter = []
513 if branch_name and not branch_ancestors:
513 if branch_name and not branch_ancestors:
514 commit_filter.append('branch("%s")' % branch_name)
514 commit_filter.append('branch("%s")' % branch_name)
515 elif branch_name and branch_ancestors:
515 elif branch_name and branch_ancestors:
516 commit_filter.append('ancestors(branch("%s"))' % branch_name)
516 commit_filter.append('ancestors(branch("%s"))' % branch_name)
517 if start_date and not end_date:
517 if start_date and not end_date:
518 commit_filter.append('date(">%s")' % start_date)
518 commit_filter.append('date(">%s")' % start_date)
519 if end_date and not start_date:
519 if end_date and not start_date:
520 commit_filter.append('date("<%s")' % end_date)
520 commit_filter.append('date("<%s")' % end_date)
521 if start_date and end_date:
521 if start_date and end_date:
522 commit_filter.append(
522 commit_filter.append(
523 'date(">%s") and date("<%s")' % (start_date, end_date))
523 'date(">%s") and date("<%s")' % (start_date, end_date))
524
524
525 # TODO: johbo: Figure out a simpler way for this solution
525 # TODO: johbo: Figure out a simpler way for this solution
526 collection_generator = CollectionGenerator
526 collection_generator = CollectionGenerator
527 if commit_filter:
527 if commit_filter:
528 commit_filter = map(safe_str, commit_filter)
528 commit_filter = map(safe_str, commit_filter)
529 revisions = self._remote.rev_range(commit_filter)
529 revisions = self._remote.rev_range(commit_filter)
530 collection_generator = MercurialIndexBasedCollectionGenerator
530 collection_generator = MercurialIndexBasedCollectionGenerator
531 else:
531 else:
532 revisions = self.commit_ids
532 revisions = self.commit_ids
533
533
534 if start_pos or end_pos:
534 if start_pos or end_pos:
535 revisions = revisions[start_pos:end_pos]
535 revisions = revisions[start_pos:end_pos]
536
536
537 return collection_generator(self, revisions, pre_load=pre_load)
537 return collection_generator(self, revisions, pre_load=pre_load)
538
538
539 def pull(self, url, commit_ids=None):
539 def pull(self, url, commit_ids=None):
540 """
540 """
541 Tries to pull changes from external location.
541 Tries to pull changes from external location.
542
542
543 :param commit_ids: Optional. Can be set to a list of commit ids
543 :param commit_ids: Optional. Can be set to a list of commit ids
544 which shall be pulled from the other repository.
544 which shall be pulled from the other repository.
545 """
545 """
546 url = self._get_url(url)
546 url = self._get_url(url)
547 self._remote.pull(url, commit_ids=commit_ids)
547 self._remote.pull(url, commit_ids=commit_ids)
548 self._remote.invalidate_vcs_cache()
548 self._remote.invalidate_vcs_cache()
549
549
550 def _local_clone(self, clone_path):
550 def _local_clone(self, clone_path):
551 """
551 """
552 Create a local clone of the current repo.
552 Create a local clone of the current repo.
553 """
553 """
554 self._remote.clone(self.path, clone_path, update_after_clone=True,
554 self._remote.clone(self.path, clone_path, update_after_clone=True,
555 hooks=False)
555 hooks=False)
556
556
557 def _update(self, revision, clean=False):
557 def _update(self, revision, clean=False):
558 """
558 """
559 Update the working copty to the specified revision.
559 Update the working copty to the specified revision.
560 """
560 """
561 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
561 self._remote.update(revision, clean=clean)
562 self._remote.update(revision, clean=clean)
562
563
563 def _identify(self):
564 def _identify(self):
564 """
565 """
565 Return the current state of the working directory.
566 Return the current state of the working directory.
566 """
567 """
567 return self._remote.identify().strip().rstrip('+')
568 return self._remote.identify().strip().rstrip('+')
568
569
569 def _heads(self, branch=None):
570 def _heads(self, branch=None):
570 """
571 """
571 Return the commit ids of the repository heads.
572 Return the commit ids of the repository heads.
572 """
573 """
573 return self._remote.heads(branch=branch).strip().split(' ')
574 return self._remote.heads(branch=branch).strip().split(' ')
574
575
575 def _ancestor(self, revision1, revision2):
576 def _ancestor(self, revision1, revision2):
576 """
577 """
577 Return the common ancestor of the two revisions.
578 Return the common ancestor of the two revisions.
578 """
579 """
579 return self._remote.ancestor(revision1, revision2)
580 return self._remote.ancestor(revision1, revision2)
580
581
581 def _local_push(
582 def _local_push(
582 self, revision, repository_path, push_branches=False,
583 self, revision, repository_path, push_branches=False,
583 enable_hooks=False):
584 enable_hooks=False):
584 """
585 """
585 Push the given revision to the specified repository.
586 Push the given revision to the specified repository.
586
587
587 :param push_branches: allow to create branches in the target repo.
588 :param push_branches: allow to create branches in the target repo.
588 """
589 """
589 self._remote.push(
590 self._remote.push(
590 [revision], repository_path, hooks=enable_hooks,
591 [revision], repository_path, hooks=enable_hooks,
591 push_branches=push_branches)
592 push_branches=push_branches)
592
593
593 def _local_merge(self, target_ref, merge_message, user_name, user_email,
594 def _local_merge(self, target_ref, merge_message, user_name, user_email,
594 source_ref, use_rebase=False):
595 source_ref, use_rebase=False):
595 """
596 """
596 Merge the given source_revision into the checked out revision.
597 Merge the given source_revision into the checked out revision.
597
598
598 Returns the commit id of the merge and a boolean indicating if the
599 Returns the commit id of the merge and a boolean indicating if the
599 commit needs to be pushed.
600 commit needs to be pushed.
600 """
601 """
601 self._update(target_ref.commit_id)
602 self._update(target_ref.commit_id)
602
603
603 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
604 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
604 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
605 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
605
606
606 if ancestor == source_ref.commit_id:
607 if ancestor == source_ref.commit_id:
607 # Nothing to do, the changes were already integrated
608 # Nothing to do, the changes were already integrated
608 return target_ref.commit_id, False
609 return target_ref.commit_id, False
609
610
610 elif ancestor == target_ref.commit_id and is_the_same_branch:
611 elif ancestor == target_ref.commit_id and is_the_same_branch:
611 # In this case we should force a commit message
612 # In this case we should force a commit message
612 return source_ref.commit_id, True
613 return source_ref.commit_id, True
613
614
614 if use_rebase:
615 if use_rebase:
615 try:
616 try:
616 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
617 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
617 target_ref.commit_id)
618 target_ref.commit_id)
618 self.bookmark(bookmark_name, revision=source_ref.commit_id)
619 self.bookmark(bookmark_name, revision=source_ref.commit_id)
619 self._remote.rebase(
620 self._remote.rebase(
620 source=source_ref.commit_id, dest=target_ref.commit_id)
621 source=source_ref.commit_id, dest=target_ref.commit_id)
621 self._remote.invalidate_vcs_cache()
622 self._remote.invalidate_vcs_cache()
622 self._update(bookmark_name)
623 self._update(bookmark_name)
623 return self._identify(), True
624 return self._identify(), True
624 except RepositoryError:
625 except RepositoryError:
625 # The rebase-abort may raise another exception which 'hides'
626 # The rebase-abort may raise another exception which 'hides'
626 # the original one, therefore we log it here.
627 # the original one, therefore we log it here.
627 log.exception('Error while rebasing shadow repo during merge.')
628 log.exception('Error while rebasing shadow repo during merge.')
628
629
629 # Cleanup any rebase leftovers
630 # Cleanup any rebase leftovers
630 self._remote.invalidate_vcs_cache()
631 self._remote.invalidate_vcs_cache()
631 self._remote.rebase(abort=True)
632 self._remote.rebase(abort=True)
632 self._remote.invalidate_vcs_cache()
633 self._remote.invalidate_vcs_cache()
633 self._remote.update(clean=True)
634 self._remote.update(clean=True)
634 raise
635 raise
635 else:
636 else:
636 try:
637 try:
637 self._remote.merge(source_ref.commit_id)
638 self._remote.merge(source_ref.commit_id)
638 self._remote.invalidate_vcs_cache()
639 self._remote.invalidate_vcs_cache()
639 self._remote.commit(
640 self._remote.commit(
640 message=safe_str(merge_message),
641 message=safe_str(merge_message),
641 username=safe_str('%s <%s>' % (user_name, user_email)))
642 username=safe_str('%s <%s>' % (user_name, user_email)))
642 self._remote.invalidate_vcs_cache()
643 self._remote.invalidate_vcs_cache()
643 return self._identify(), True
644 return self._identify(), True
644 except RepositoryError:
645 except RepositoryError:
645 # Cleanup any merge leftovers
646 # Cleanup any merge leftovers
646 self._remote.update(clean=True)
647 self._remote.update(clean=True)
647 raise
648 raise
648
649
649 def _local_close(self, target_ref, user_name, user_email,
650 def _local_close(self, target_ref, user_name, user_email,
650 source_ref, close_message=''):
651 source_ref, close_message=''):
651 """
652 """
652 Close the branch of the given source_revision
653 Close the branch of the given source_revision
653
654
654 Returns the commit id of the close and a boolean indicating if the
655 Returns the commit id of the close and a boolean indicating if the
655 commit needs to be pushed.
656 commit needs to be pushed.
656 """
657 """
657 self._update(target_ref.commit_id)
658 self._update(source_ref.commit_id)
658 message = close_message or "Closing branch"
659 message = close_message or "Closing branch: `{}`".format(source_ref.name)
659 try:
660 try:
660 self._remote.commit(
661 self._remote.commit(
661 message=safe_str(message),
662 message=safe_str(message),
662 username=safe_str('%s <%s>' % (user_name, user_email)),
663 username=safe_str('%s <%s>' % (user_name, user_email)),
663 close_branch=True)
664 close_branch=True)
664 self._remote.invalidate_vcs_cache()
665 self._remote.invalidate_vcs_cache()
665 return self._identify(), True
666 return self._identify(), True
666 except RepositoryError:
667 except RepositoryError:
667 # Cleanup any commit leftovers
668 # Cleanup any commit leftovers
668 self._remote.update(clean=True)
669 self._remote.update(clean=True)
669 raise
670 raise
670
671
671 def _is_the_same_branch(self, target_ref, source_ref):
672 def _is_the_same_branch(self, target_ref, source_ref):
672 return (
673 return (
673 self._get_branch_name(target_ref) ==
674 self._get_branch_name(target_ref) ==
674 self._get_branch_name(source_ref))
675 self._get_branch_name(source_ref))
675
676
676 def _get_branch_name(self, ref):
677 def _get_branch_name(self, ref):
677 if ref.type == 'branch':
678 if ref.type == 'branch':
678 return ref.name
679 return ref.name
679 return self._remote.ctx_branch(ref.commit_id)
680 return self._remote.ctx_branch(ref.commit_id)
680
681
681 def _get_shadow_repository_path(self, workspace_id):
682 def _get_shadow_repository_path(self, workspace_id):
682 # The name of the shadow repository must start with '.', so it is
683 # The name of the shadow repository must start with '.', so it is
683 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
684 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
684 return os.path.join(
685 return os.path.join(
685 os.path.dirname(self.path),
686 os.path.dirname(self.path),
686 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
687 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
687
688
688 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
689 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
689 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
690 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
690 if not os.path.exists(shadow_repository_path):
691 if not os.path.exists(shadow_repository_path):
691 self._local_clone(shadow_repository_path)
692 self._local_clone(shadow_repository_path)
692 log.debug(
693 log.debug(
693 'Prepared shadow repository in %s', shadow_repository_path)
694 'Prepared shadow repository in %s', shadow_repository_path)
694
695
695 return shadow_repository_path
696 return shadow_repository_path
696
697
697 def cleanup_merge_workspace(self, workspace_id):
698 def cleanup_merge_workspace(self, workspace_id):
698 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
699 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
699 shutil.rmtree(shadow_repository_path, ignore_errors=True)
700 shutil.rmtree(shadow_repository_path, ignore_errors=True)
700
701
701 def _merge_repo(self, shadow_repository_path, target_ref,
702 def _merge_repo(self, shadow_repository_path, target_ref,
702 source_repo, source_ref, merge_message,
703 source_repo, source_ref, merge_message,
703 merger_name, merger_email, dry_run=False,
704 merger_name, merger_email, dry_run=False,
704 use_rebase=False, close_branch=False):
705 use_rebase=False, close_branch=False):
705 if target_ref.commit_id not in self._heads():
706 if target_ref.commit_id not in self._heads():
706 return MergeResponse(
707 return MergeResponse(
707 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
708 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
708
709
709 try:
710 try:
710 if (target_ref.type == 'branch' and
711 if (target_ref.type == 'branch' and
711 len(self._heads(target_ref.name)) != 1):
712 len(self._heads(target_ref.name)) != 1):
712 return MergeResponse(
713 return MergeResponse(
713 False, False, None,
714 False, False, None,
714 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
715 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
715 except CommitDoesNotExistError as e:
716 except CommitDoesNotExistError as e:
716 log.exception('Failure when looking up branch heads on hg target')
717 log.exception('Failure when looking up branch heads on hg target')
717 return MergeResponse(
718 return MergeResponse(
718 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
719 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
719
720
720 shadow_repo = self._get_shadow_instance(shadow_repository_path)
721 shadow_repo = self._get_shadow_instance(shadow_repository_path)
721
722
722 log.debug('Pulling in target reference %s', target_ref)
723 log.debug('Pulling in target reference %s', target_ref)
723 self._validate_pull_reference(target_ref)
724 self._validate_pull_reference(target_ref)
724 shadow_repo._local_pull(self.path, target_ref)
725 shadow_repo._local_pull(self.path, target_ref)
725 try:
726 try:
726 log.debug('Pulling in source reference %s', source_ref)
727 log.debug('Pulling in source reference %s', source_ref)
727 source_repo._validate_pull_reference(source_ref)
728 source_repo._validate_pull_reference(source_ref)
728 shadow_repo._local_pull(source_repo.path, source_ref)
729 shadow_repo._local_pull(source_repo.path, source_ref)
729 except CommitDoesNotExistError:
730 except CommitDoesNotExistError:
730 log.exception('Failure when doing local pull on hg shadow repo')
731 log.exception('Failure when doing local pull on hg shadow repo')
731 return MergeResponse(
732 return MergeResponse(
732 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
733 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
733
734
734 merge_ref = None
735 merge_ref = None
736 merge_commit_id = None
737 close_commit_id = None
735 merge_failure_reason = MergeFailureReason.NONE
738 merge_failure_reason = MergeFailureReason.NONE
736
739
737 if close_branch and not use_rebase:
740 # enforce that close branch should be used only in case we source from
741 # an actual Branch
742 close_branch = close_branch and source_ref.type == 'branch'
743
744 # don't allow to close branch if source and target are the same
745 close_branch = close_branch and source_ref.name != target_ref.name
746
747 needs_push_on_close = False
748 if close_branch and not use_rebase and not dry_run:
738 try:
749 try:
739 close_commit_id, needs_push = shadow_repo._local_close(
750 close_commit_id, needs_push_on_close = shadow_repo._local_close(
740 target_ref, merger_name, merger_email, source_ref)
751 target_ref, merger_name, merger_email, source_ref)
741 target_ref.commit_id = close_commit_id
742 merge_possible = True
752 merge_possible = True
743 except RepositoryError:
753 except RepositoryError:
744 log.exception('Failure when doing close branch on hg shadow repo')
754 log.exception(
755 'Failure when doing close branch on hg shadow repo')
745 merge_possible = False
756 merge_possible = False
746 merge_failure_reason = MergeFailureReason.MERGE_FAILED
757 merge_failure_reason = MergeFailureReason.MERGE_FAILED
747 else:
758 else:
748 merge_possible = True
759 merge_possible = True
749
760
750 if merge_possible:
761 if merge_possible:
751 try:
762 try:
752 merge_commit_id, needs_push = shadow_repo._local_merge(
763 merge_commit_id, needs_push = shadow_repo._local_merge(
753 target_ref, merge_message, merger_name, merger_email,
764 target_ref, merge_message, merger_name, merger_email,
754 source_ref, use_rebase=use_rebase)
765 source_ref, use_rebase=use_rebase)
755 merge_possible = True
766 merge_possible = True
756
767
757 # Set a bookmark pointing to the merge commit. This bookmark may be
768 # read the state of the close action, if it
758 # used to easily identify the last successful merge commit in the
769 # maybe required a push
759 # shadow repository.
770 needs_push = needs_push or needs_push_on_close
771
772 # Set a bookmark pointing to the merge commit. This bookmark
773 # may be used to easily identify the last successful merge
774 # commit in the shadow repository.
760 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
775 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
761 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
776 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
762 except SubrepoMergeError:
777 except SubrepoMergeError:
763 log.exception(
778 log.exception(
764 'Subrepo merge error during local merge on hg shadow repo.')
779 'Subrepo merge error during local merge on hg shadow repo.')
765 merge_possible = False
780 merge_possible = False
766 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
781 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
767 except RepositoryError:
782 except RepositoryError:
768 log.exception('Failure when doing local merge on hg shadow repo')
783 log.exception('Failure when doing local merge on hg shadow repo')
769 merge_possible = False
784 merge_possible = False
770 merge_failure_reason = MergeFailureReason.MERGE_FAILED
785 merge_failure_reason = MergeFailureReason.MERGE_FAILED
771
786
772 if merge_possible and not dry_run:
787 if merge_possible and not dry_run:
773 if needs_push:
788 if needs_push:
774 # In case the target is a bookmark, update it, so after pushing
789 # In case the target is a bookmark, update it, so after pushing
775 # the bookmarks is also updated in the target.
790 # the bookmarks is also updated in the target.
776 if target_ref.type == 'book':
791 if target_ref.type == 'book':
777 shadow_repo.bookmark(
792 shadow_repo.bookmark(
778 target_ref.name, revision=merge_commit_id)
793 target_ref.name, revision=merge_commit_id)
779
780 try:
794 try:
781 shadow_repo_with_hooks = self._get_shadow_instance(
795 shadow_repo_with_hooks = self._get_shadow_instance(
782 shadow_repository_path,
796 shadow_repository_path,
783 enable_hooks=True)
797 enable_hooks=True)
784 # Note: the push_branches option will push any new branch
798 # Note: the push_branches option will push any new branch
785 # defined in the source repository to the target. This may
799 # defined in the source repository to the target. This may
786 # be dangerous as branches are permanent in Mercurial.
800 # be dangerous as branches are permanent in Mercurial.
787 # This feature was requested in issue #441.
801 # This feature was requested in issue #441.
788 shadow_repo_with_hooks._local_push(
802 shadow_repo_with_hooks._local_push(
789 merge_commit_id, self.path, push_branches=True,
803 merge_commit_id, self.path, push_branches=True,
790 enable_hooks=True)
804 enable_hooks=True)
805
806 # maybe we also need to push the close_commit_id
807 if close_commit_id:
808 shadow_repo_with_hooks._local_push(
809 close_commit_id, self.path, push_branches=True,
810 enable_hooks=True)
791 merge_succeeded = True
811 merge_succeeded = True
792 except RepositoryError:
812 except RepositoryError:
793 log.exception(
813 log.exception(
794 'Failure when doing local push from the shadow '
814 'Failure when doing local push from the shadow '
795 'repository to the target repository.')
815 'repository to the target repository.')
796 merge_succeeded = False
816 merge_succeeded = False
797 merge_failure_reason = MergeFailureReason.PUSH_FAILED
817 merge_failure_reason = MergeFailureReason.PUSH_FAILED
798 else:
818 else:
799 merge_succeeded = True
819 merge_succeeded = True
800 else:
820 else:
801 merge_succeeded = False
821 merge_succeeded = False
802
822
803 return MergeResponse(
823 return MergeResponse(
804 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
824 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
805
825
806 def _get_shadow_instance(
826 def _get_shadow_instance(
807 self, shadow_repository_path, enable_hooks=False):
827 self, shadow_repository_path, enable_hooks=False):
808 config = self.config.copy()
828 config = self.config.copy()
809 if not enable_hooks:
829 if not enable_hooks:
810 config.clear_section('hooks')
830 config.clear_section('hooks')
811 return MercurialRepository(shadow_repository_path, config)
831 return MercurialRepository(shadow_repository_path, config)
812
832
813 def _validate_pull_reference(self, reference):
833 def _validate_pull_reference(self, reference):
814 if not (reference.name in self.bookmarks or
834 if not (reference.name in self.bookmarks or
815 reference.name in self.branches or
835 reference.name in self.branches or
816 self.get_commit(reference.commit_id)):
836 self.get_commit(reference.commit_id)):
817 raise CommitDoesNotExistError(
837 raise CommitDoesNotExistError(
818 'Unknown branch, bookmark or commit id')
838 'Unknown branch, bookmark or commit id')
819
839
820 def _local_pull(self, repository_path, reference):
840 def _local_pull(self, repository_path, reference):
821 """
841 """
822 Fetch a branch, bookmark or commit from a local repository.
842 Fetch a branch, bookmark or commit from a local repository.
823 """
843 """
824 repository_path = os.path.abspath(repository_path)
844 repository_path = os.path.abspath(repository_path)
825 if repository_path == self.path:
845 if repository_path == self.path:
826 raise ValueError('Cannot pull from the same repository')
846 raise ValueError('Cannot pull from the same repository')
827
847
828 reference_type_to_option_name = {
848 reference_type_to_option_name = {
829 'book': 'bookmark',
849 'book': 'bookmark',
830 'branch': 'branch',
850 'branch': 'branch',
831 }
851 }
832 option_name = reference_type_to_option_name.get(
852 option_name = reference_type_to_option_name.get(
833 reference.type, 'revision')
853 reference.type, 'revision')
834
854
835 if option_name == 'revision':
855 if option_name == 'revision':
836 ref = reference.commit_id
856 ref = reference.commit_id
837 else:
857 else:
838 ref = reference.name
858 ref = reference.name
839
859
840 options = {option_name: [ref]}
860 options = {option_name: [ref]}
841 self._remote.pull_cmd(repository_path, hooks=False, **options)
861 self._remote.pull_cmd(repository_path, hooks=False, **options)
842 self._remote.invalidate_vcs_cache()
862 self._remote.invalidate_vcs_cache()
843
863
844 def bookmark(self, bookmark, revision=None):
864 def bookmark(self, bookmark, revision=None):
845 if isinstance(bookmark, unicode):
865 if isinstance(bookmark, unicode):
846 bookmark = safe_str(bookmark)
866 bookmark = safe_str(bookmark)
847 self._remote.bookmark(bookmark, revision=revision)
867 self._remote.bookmark(bookmark, revision=revision)
848 self._remote.invalidate_vcs_cache()
868 self._remote.invalidate_vcs_cache()
849
869
850
870
851 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
871 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
852
872
853 def _commit_factory(self, commit_id):
873 def _commit_factory(self, commit_id):
854 return self.repo.get_commit(
874 return self.repo.get_commit(
855 commit_idx=commit_id, pre_load=self.pre_load)
875 commit_idx=commit_id, pre_load=self.pre_load)
@@ -1,567 +1,569 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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 this is forms validation classes
22 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
23 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
24 for list off all availible validators
25
25
26 we can create our own validators
26 we can create our own validators
27
27
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
29 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
30 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35
35
36
36
37 <name> = formencode.validators.<name of validator>
37 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
38 <name> must equal form name
39 list=[1,2,3,4,5]
39 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
40 for SELECT use formencode.All(OneOf(list), Int())
41
41
42 """
42 """
43
43
44 import deform
44 import deform
45 import logging
45 import logging
46 import formencode
46 import formencode
47
47
48 from pkg_resources import resource_filename
48 from pkg_resources import resource_filename
49 from formencode import All, Pipe
49 from formencode import All, Pipe
50
50
51 from pylons.i18n.translation import _
51 from pylons.i18n.translation import _
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53
53
54 from rhodecode import BACKENDS
54 from rhodecode import BACKENDS
55 from rhodecode.lib import helpers
55 from rhodecode.lib import helpers
56 from rhodecode.model import validators as v
56 from rhodecode.model import validators as v
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 deform_templates = resource_filename('deform', 'templates')
61 deform_templates = resource_filename('deform', 'templates')
62 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
63 search_path = (rhodecode_templates, deform_templates)
63 search_path = (rhodecode_templates, deform_templates)
64
64
65
65
66 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
67 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
68 def __call__(self, template_name, **kw):
68 def __call__(self, template_name, **kw):
69 kw['h'] = helpers
69 kw['h'] = helpers
70 kw['request'] = get_current_request()
70 kw['request'] = get_current_request()
71 return self.load(template_name)(**kw)
71 return self.load(template_name)(**kw)
72
72
73
73
74 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
74 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
75 deform.Form.set_default_renderer(form_renderer)
75 deform.Form.set_default_renderer(form_renderer)
76
76
77
77
78 def LoginForm():
78 def LoginForm():
79 class _LoginForm(formencode.Schema):
79 class _LoginForm(formencode.Schema):
80 allow_extra_fields = True
80 allow_extra_fields = True
81 filter_extra_fields = True
81 filter_extra_fields = True
82 username = v.UnicodeString(
82 username = v.UnicodeString(
83 strip=True,
83 strip=True,
84 min=1,
84 min=1,
85 not_empty=True,
85 not_empty=True,
86 messages={
86 messages={
87 'empty': _(u'Please enter a login'),
87 'empty': _(u'Please enter a login'),
88 'tooShort': _(u'Enter a value %(min)i characters long or more')
88 'tooShort': _(u'Enter a value %(min)i characters long or more')
89 }
89 }
90 )
90 )
91
91
92 password = v.UnicodeString(
92 password = v.UnicodeString(
93 strip=False,
93 strip=False,
94 min=3,
94 min=3,
95 not_empty=True,
95 not_empty=True,
96 messages={
96 messages={
97 'empty': _(u'Please enter a password'),
97 'empty': _(u'Please enter a password'),
98 'tooShort': _(u'Enter %(min)i characters or more')}
98 'tooShort': _(u'Enter %(min)i characters or more')}
99 )
99 )
100
100
101 remember = v.StringBoolean(if_missing=False)
101 remember = v.StringBoolean(if_missing=False)
102
102
103 chained_validators = [v.ValidAuth()]
103 chained_validators = [v.ValidAuth()]
104 return _LoginForm
104 return _LoginForm
105
105
106
106
107 def UserForm(edit=False, available_languages=[], old_data={}):
107 def UserForm(edit=False, available_languages=[], old_data={}):
108 class _UserForm(formencode.Schema):
108 class _UserForm(formencode.Schema):
109 allow_extra_fields = True
109 allow_extra_fields = True
110 filter_extra_fields = True
110 filter_extra_fields = True
111 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
111 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
112 v.ValidUsername(edit, old_data))
112 v.ValidUsername(edit, old_data))
113 if edit:
113 if edit:
114 new_password = All(
114 new_password = All(
115 v.ValidPassword(),
115 v.ValidPassword(),
116 v.UnicodeString(strip=False, min=6, not_empty=False)
116 v.UnicodeString(strip=False, min=6, not_empty=False)
117 )
117 )
118 password_confirmation = All(
118 password_confirmation = All(
119 v.ValidPassword(),
119 v.ValidPassword(),
120 v.UnicodeString(strip=False, min=6, not_empty=False),
120 v.UnicodeString(strip=False, min=6, not_empty=False),
121 )
121 )
122 admin = v.StringBoolean(if_missing=False)
122 admin = v.StringBoolean(if_missing=False)
123 else:
123 else:
124 password = All(
124 password = All(
125 v.ValidPassword(),
125 v.ValidPassword(),
126 v.UnicodeString(strip=False, min=6, not_empty=True)
126 v.UnicodeString(strip=False, min=6, not_empty=True)
127 )
127 )
128 password_confirmation = All(
128 password_confirmation = All(
129 v.ValidPassword(),
129 v.ValidPassword(),
130 v.UnicodeString(strip=False, min=6, not_empty=False)
130 v.UnicodeString(strip=False, min=6, not_empty=False)
131 )
131 )
132
132
133 password_change = v.StringBoolean(if_missing=False)
133 password_change = v.StringBoolean(if_missing=False)
134 create_repo_group = v.StringBoolean(if_missing=False)
134 create_repo_group = v.StringBoolean(if_missing=False)
135
135
136 active = v.StringBoolean(if_missing=False)
136 active = v.StringBoolean(if_missing=False)
137 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
138 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
138 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
139 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
139 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
140 extern_name = v.UnicodeString(strip=True)
140 extern_name = v.UnicodeString(strip=True)
141 extern_type = v.UnicodeString(strip=True)
141 extern_type = v.UnicodeString(strip=True)
142 language = v.OneOf(available_languages, hideList=False,
142 language = v.OneOf(available_languages, hideList=False,
143 testValueList=True, if_missing=None)
143 testValueList=True, if_missing=None)
144 chained_validators = [v.ValidPasswordsMatch()]
144 chained_validators = [v.ValidPasswordsMatch()]
145 return _UserForm
145 return _UserForm
146
146
147
147
148 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
148 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
149 old_data = old_data or {}
149 old_data = old_data or {}
150
150
151 class _UserGroupForm(formencode.Schema):
151 class _UserGroupForm(formencode.Schema):
152 allow_extra_fields = True
152 allow_extra_fields = True
153 filter_extra_fields = True
153 filter_extra_fields = True
154
154
155 users_group_name = All(
155 users_group_name = All(
156 v.UnicodeString(strip=True, min=1, not_empty=True),
156 v.UnicodeString(strip=True, min=1, not_empty=True),
157 v.ValidUserGroup(edit, old_data)
157 v.ValidUserGroup(edit, old_data)
158 )
158 )
159 user_group_description = v.UnicodeString(strip=True, min=1,
159 user_group_description = v.UnicodeString(strip=True, min=1,
160 not_empty=False)
160 not_empty=False)
161
161
162 users_group_active = v.StringBoolean(if_missing=False)
162 users_group_active = v.StringBoolean(if_missing=False)
163
163
164 if edit:
164 if edit:
165 # this is user group owner
165 # this is user group owner
166 user = All(
166 user = All(
167 v.UnicodeString(not_empty=True),
167 v.UnicodeString(not_empty=True),
168 v.ValidRepoUser(allow_disabled))
168 v.ValidRepoUser(allow_disabled))
169 return _UserGroupForm
169 return _UserGroupForm
170
170
171
171
172 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
172 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
173 can_create_in_root=False, allow_disabled=False):
173 can_create_in_root=False, allow_disabled=False):
174 old_data = old_data or {}
174 old_data = old_data or {}
175 available_groups = available_groups or []
175 available_groups = available_groups or []
176
176
177 class _RepoGroupForm(formencode.Schema):
177 class _RepoGroupForm(formencode.Schema):
178 allow_extra_fields = True
178 allow_extra_fields = True
179 filter_extra_fields = False
179 filter_extra_fields = False
180
180
181 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
181 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
182 v.SlugifyName(),)
182 v.SlugifyName(),)
183 group_description = v.UnicodeString(strip=True, min=1,
183 group_description = v.UnicodeString(strip=True, min=1,
184 not_empty=False)
184 not_empty=False)
185 group_copy_permissions = v.StringBoolean(if_missing=False)
185 group_copy_permissions = v.StringBoolean(if_missing=False)
186
186
187 group_parent_id = v.OneOf(available_groups, hideList=False,
187 group_parent_id = v.OneOf(available_groups, hideList=False,
188 testValueList=True, not_empty=True)
188 testValueList=True, not_empty=True)
189 enable_locking = v.StringBoolean(if_missing=False)
189 enable_locking = v.StringBoolean(if_missing=False)
190 chained_validators = [
190 chained_validators = [
191 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
191 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
192
192
193 if edit:
193 if edit:
194 # this is repo group owner
194 # this is repo group owner
195 user = All(
195 user = All(
196 v.UnicodeString(not_empty=True),
196 v.UnicodeString(not_empty=True),
197 v.ValidRepoUser(allow_disabled))
197 v.ValidRepoUser(allow_disabled))
198
198
199 return _RepoGroupForm
199 return _RepoGroupForm
200
200
201
201
202 def RegisterForm(edit=False, old_data={}):
202 def RegisterForm(edit=False, old_data={}):
203 class _RegisterForm(formencode.Schema):
203 class _RegisterForm(formencode.Schema):
204 allow_extra_fields = True
204 allow_extra_fields = True
205 filter_extra_fields = True
205 filter_extra_fields = True
206 username = All(
206 username = All(
207 v.ValidUsername(edit, old_data),
207 v.ValidUsername(edit, old_data),
208 v.UnicodeString(strip=True, min=1, not_empty=True)
208 v.UnicodeString(strip=True, min=1, not_empty=True)
209 )
209 )
210 password = All(
210 password = All(
211 v.ValidPassword(),
211 v.ValidPassword(),
212 v.UnicodeString(strip=False, min=6, not_empty=True)
212 v.UnicodeString(strip=False, min=6, not_empty=True)
213 )
213 )
214 password_confirmation = All(
214 password_confirmation = All(
215 v.ValidPassword(),
215 v.ValidPassword(),
216 v.UnicodeString(strip=False, min=6, not_empty=True)
216 v.UnicodeString(strip=False, min=6, not_empty=True)
217 )
217 )
218 active = v.StringBoolean(if_missing=False)
218 active = v.StringBoolean(if_missing=False)
219 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
219 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
220 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
220 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
221 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
221 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
222
222
223 chained_validators = [v.ValidPasswordsMatch()]
223 chained_validators = [v.ValidPasswordsMatch()]
224
224
225 return _RegisterForm
225 return _RegisterForm
226
226
227
227
228 def PasswordResetForm():
228 def PasswordResetForm():
229 class _PasswordResetForm(formencode.Schema):
229 class _PasswordResetForm(formencode.Schema):
230 allow_extra_fields = True
230 allow_extra_fields = True
231 filter_extra_fields = True
231 filter_extra_fields = True
232 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
232 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
233 return _PasswordResetForm
233 return _PasswordResetForm
234
234
235
235
236 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
236 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
237 allow_disabled=False):
237 allow_disabled=False):
238 old_data = old_data or {}
238 old_data = old_data or {}
239 repo_groups = repo_groups or []
239 repo_groups = repo_groups or []
240 landing_revs = landing_revs or []
240 landing_revs = landing_revs or []
241 supported_backends = BACKENDS.keys()
241 supported_backends = BACKENDS.keys()
242
242
243 class _RepoForm(formencode.Schema):
243 class _RepoForm(formencode.Schema):
244 allow_extra_fields = True
244 allow_extra_fields = True
245 filter_extra_fields = False
245 filter_extra_fields = False
246 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
246 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
247 v.SlugifyName(), v.CannotHaveGitSuffix())
247 v.SlugifyName(), v.CannotHaveGitSuffix())
248 repo_group = All(v.CanWriteGroup(old_data),
248 repo_group = All(v.CanWriteGroup(old_data),
249 v.OneOf(repo_groups, hideList=True))
249 v.OneOf(repo_groups, hideList=True))
250 repo_type = v.OneOf(supported_backends, required=False,
250 repo_type = v.OneOf(supported_backends, required=False,
251 if_missing=old_data.get('repo_type'))
251 if_missing=old_data.get('repo_type'))
252 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
252 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
253 repo_private = v.StringBoolean(if_missing=False)
253 repo_private = v.StringBoolean(if_missing=False)
254 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
254 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
255 repo_copy_permissions = v.StringBoolean(if_missing=False)
255 repo_copy_permissions = v.StringBoolean(if_missing=False)
256 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
256 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
257
257
258 repo_enable_statistics = v.StringBoolean(if_missing=False)
258 repo_enable_statistics = v.StringBoolean(if_missing=False)
259 repo_enable_downloads = v.StringBoolean(if_missing=False)
259 repo_enable_downloads = v.StringBoolean(if_missing=False)
260 repo_enable_locking = v.StringBoolean(if_missing=False)
260 repo_enable_locking = v.StringBoolean(if_missing=False)
261
261
262 if edit:
262 if edit:
263 # this is repo owner
263 # this is repo owner
264 user = All(
264 user = All(
265 v.UnicodeString(not_empty=True),
265 v.UnicodeString(not_empty=True),
266 v.ValidRepoUser(allow_disabled))
266 v.ValidRepoUser(allow_disabled))
267 clone_uri_change = v.UnicodeString(
267 clone_uri_change = v.UnicodeString(
268 not_empty=False, if_missing=v.Missing)
268 not_empty=False, if_missing=v.Missing)
269
269
270 chained_validators = [v.ValidCloneUri(),
270 chained_validators = [v.ValidCloneUri(),
271 v.ValidRepoName(edit, old_data)]
271 v.ValidRepoName(edit, old_data)]
272 return _RepoForm
272 return _RepoForm
273
273
274
274
275 def RepoPermsForm():
275 def RepoPermsForm():
276 class _RepoPermsForm(formencode.Schema):
276 class _RepoPermsForm(formencode.Schema):
277 allow_extra_fields = True
277 allow_extra_fields = True
278 filter_extra_fields = False
278 filter_extra_fields = False
279 chained_validators = [v.ValidPerms(type_='repo')]
279 chained_validators = [v.ValidPerms(type_='repo')]
280 return _RepoPermsForm
280 return _RepoPermsForm
281
281
282
282
283 def RepoGroupPermsForm(valid_recursive_choices):
283 def RepoGroupPermsForm(valid_recursive_choices):
284 class _RepoGroupPermsForm(formencode.Schema):
284 class _RepoGroupPermsForm(formencode.Schema):
285 allow_extra_fields = True
285 allow_extra_fields = True
286 filter_extra_fields = False
286 filter_extra_fields = False
287 recursive = v.OneOf(valid_recursive_choices)
287 recursive = v.OneOf(valid_recursive_choices)
288 chained_validators = [v.ValidPerms(type_='repo_group')]
288 chained_validators = [v.ValidPerms(type_='repo_group')]
289 return _RepoGroupPermsForm
289 return _RepoGroupPermsForm
290
290
291
291
292 def UserGroupPermsForm():
292 def UserGroupPermsForm():
293 class _UserPermsForm(formencode.Schema):
293 class _UserPermsForm(formencode.Schema):
294 allow_extra_fields = True
294 allow_extra_fields = True
295 filter_extra_fields = False
295 filter_extra_fields = False
296 chained_validators = [v.ValidPerms(type_='user_group')]
296 chained_validators = [v.ValidPerms(type_='user_group')]
297 return _UserPermsForm
297 return _UserPermsForm
298
298
299
299
300 def RepoFieldForm():
300 def RepoFieldForm():
301 class _RepoFieldForm(formencode.Schema):
301 class _RepoFieldForm(formencode.Schema):
302 filter_extra_fields = True
302 filter_extra_fields = True
303 allow_extra_fields = True
303 allow_extra_fields = True
304
304
305 new_field_key = All(v.FieldKey(),
305 new_field_key = All(v.FieldKey(),
306 v.UnicodeString(strip=True, min=3, not_empty=True))
306 v.UnicodeString(strip=True, min=3, not_empty=True))
307 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
307 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
308 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
308 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
309 if_missing='str')
309 if_missing='str')
310 new_field_label = v.UnicodeString(not_empty=False)
310 new_field_label = v.UnicodeString(not_empty=False)
311 new_field_desc = v.UnicodeString(not_empty=False)
311 new_field_desc = v.UnicodeString(not_empty=False)
312
312
313 return _RepoFieldForm
313 return _RepoFieldForm
314
314
315
315
316 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
316 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
317 repo_groups=[], landing_revs=[]):
317 repo_groups=[], landing_revs=[]):
318 class _RepoForkForm(formencode.Schema):
318 class _RepoForkForm(formencode.Schema):
319 allow_extra_fields = True
319 allow_extra_fields = True
320 filter_extra_fields = False
320 filter_extra_fields = False
321 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
321 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
322 v.SlugifyName())
322 v.SlugifyName())
323 repo_group = All(v.CanWriteGroup(),
323 repo_group = All(v.CanWriteGroup(),
324 v.OneOf(repo_groups, hideList=True))
324 v.OneOf(repo_groups, hideList=True))
325 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
325 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
326 description = v.UnicodeString(strip=True, min=1, not_empty=True)
326 description = v.UnicodeString(strip=True, min=1, not_empty=True)
327 private = v.StringBoolean(if_missing=False)
327 private = v.StringBoolean(if_missing=False)
328 copy_permissions = v.StringBoolean(if_missing=False)
328 copy_permissions = v.StringBoolean(if_missing=False)
329 fork_parent_id = v.UnicodeString()
329 fork_parent_id = v.UnicodeString()
330 chained_validators = [v.ValidForkName(edit, old_data)]
330 chained_validators = [v.ValidForkName(edit, old_data)]
331 landing_rev = v.OneOf(landing_revs, hideList=True)
331 landing_rev = v.OneOf(landing_revs, hideList=True)
332
332
333 return _RepoForkForm
333 return _RepoForkForm
334
334
335
335
336 def ApplicationSettingsForm():
336 def ApplicationSettingsForm():
337 class _ApplicationSettingsForm(formencode.Schema):
337 class _ApplicationSettingsForm(formencode.Schema):
338 allow_extra_fields = True
338 allow_extra_fields = True
339 filter_extra_fields = False
339 filter_extra_fields = False
340 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
340 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
341 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
341 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
342 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
342 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
343 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
343 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
344 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
344 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
345 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
345 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
346 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
346 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
347 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
347 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
348
348
349 return _ApplicationSettingsForm
349 return _ApplicationSettingsForm
350
350
351
351
352 def ApplicationVisualisationForm():
352 def ApplicationVisualisationForm():
353 class _ApplicationVisualisationForm(formencode.Schema):
353 class _ApplicationVisualisationForm(formencode.Schema):
354 allow_extra_fields = True
354 allow_extra_fields = True
355 filter_extra_fields = False
355 filter_extra_fields = False
356 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
356 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
357 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
357 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
358 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
358 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
359
359
360 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
360 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
361 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
361 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
362 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
362 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
363 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
363 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
364 rhodecode_show_version = v.StringBoolean(if_missing=False)
364 rhodecode_show_version = v.StringBoolean(if_missing=False)
365 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
365 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
366 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
366 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
367 rhodecode_gravatar_url = v.UnicodeString(min=3)
367 rhodecode_gravatar_url = v.UnicodeString(min=3)
368 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
368 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
369 rhodecode_support_url = v.UnicodeString()
369 rhodecode_support_url = v.UnicodeString()
370 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
370 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
371 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
371 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
372
372
373 return _ApplicationVisualisationForm
373 return _ApplicationVisualisationForm
374
374
375
375
376 class _BaseVcsSettingsForm(formencode.Schema):
376 class _BaseVcsSettingsForm(formencode.Schema):
377 allow_extra_fields = True
377 allow_extra_fields = True
378 filter_extra_fields = False
378 filter_extra_fields = False
379 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
379 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
380 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
380 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
381 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
381 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
382
382
383 # PR/Code-review
383 # PR/Code-review
384 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
384 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
385 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
385 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
386
386
387 # hg
387 # hg
388 extensions_largefiles = v.StringBoolean(if_missing=False)
388 extensions_largefiles = v.StringBoolean(if_missing=False)
389 extensions_evolve = v.StringBoolean(if_missing=False)
389 extensions_evolve = v.StringBoolean(if_missing=False)
390 phases_publish = v.StringBoolean(if_missing=False)
390 phases_publish = v.StringBoolean(if_missing=False)
391
391
392 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
392 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
393 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
393 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
394
394
395 # git
395 # git
396 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
396 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
397 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
398 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
397
399
398 # svn
400 # svn
399 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
401 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
400 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
402 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
401
403
402
404
403 def ApplicationUiSettingsForm():
405 def ApplicationUiSettingsForm():
404 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
406 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
405 web_push_ssl = v.StringBoolean(if_missing=False)
407 web_push_ssl = v.StringBoolean(if_missing=False)
406 paths_root_path = All(
408 paths_root_path = All(
407 v.ValidPath(),
409 v.ValidPath(),
408 v.UnicodeString(strip=True, min=1, not_empty=True)
410 v.UnicodeString(strip=True, min=1, not_empty=True)
409 )
411 )
410 largefiles_usercache = All(
412 largefiles_usercache = All(
411 v.ValidPath(),
413 v.ValidPath(),
412 v.UnicodeString(strip=True, min=2, not_empty=True))
414 v.UnicodeString(strip=True, min=2, not_empty=True))
413 vcs_git_lfs_store_location = All(
415 vcs_git_lfs_store_location = All(
414 v.ValidPath(),
416 v.ValidPath(),
415 v.UnicodeString(strip=True, min=2, not_empty=True))
417 v.UnicodeString(strip=True, min=2, not_empty=True))
416 extensions_hgsubversion = v.StringBoolean(if_missing=False)
418 extensions_hgsubversion = v.StringBoolean(if_missing=False)
417 extensions_hggit = v.StringBoolean(if_missing=False)
419 extensions_hggit = v.StringBoolean(if_missing=False)
418 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
420 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
419 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
421 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
420
422
421 return _ApplicationUiSettingsForm
423 return _ApplicationUiSettingsForm
422
424
423
425
424 def RepoVcsSettingsForm(repo_name):
426 def RepoVcsSettingsForm(repo_name):
425 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
427 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
426 inherit_global_settings = v.StringBoolean(if_missing=False)
428 inherit_global_settings = v.StringBoolean(if_missing=False)
427 new_svn_branch = v.ValidSvnPattern(
429 new_svn_branch = v.ValidSvnPattern(
428 section='vcs_svn_branch', repo_name=repo_name)
430 section='vcs_svn_branch', repo_name=repo_name)
429 new_svn_tag = v.ValidSvnPattern(
431 new_svn_tag = v.ValidSvnPattern(
430 section='vcs_svn_tag', repo_name=repo_name)
432 section='vcs_svn_tag', repo_name=repo_name)
431
433
432 return _RepoVcsSettingsForm
434 return _RepoVcsSettingsForm
433
435
434
436
435 def LabsSettingsForm():
437 def LabsSettingsForm():
436 class _LabSettingsForm(formencode.Schema):
438 class _LabSettingsForm(formencode.Schema):
437 allow_extra_fields = True
439 allow_extra_fields = True
438 filter_extra_fields = False
440 filter_extra_fields = False
439
441
440 return _LabSettingsForm
442 return _LabSettingsForm
441
443
442
444
443 def ApplicationPermissionsForm(
445 def ApplicationPermissionsForm(
444 register_choices, password_reset_choices, extern_activate_choices):
446 register_choices, password_reset_choices, extern_activate_choices):
445 class _DefaultPermissionsForm(formencode.Schema):
447 class _DefaultPermissionsForm(formencode.Schema):
446 allow_extra_fields = True
448 allow_extra_fields = True
447 filter_extra_fields = True
449 filter_extra_fields = True
448
450
449 anonymous = v.StringBoolean(if_missing=False)
451 anonymous = v.StringBoolean(if_missing=False)
450 default_register = v.OneOf(register_choices)
452 default_register = v.OneOf(register_choices)
451 default_register_message = v.UnicodeString()
453 default_register_message = v.UnicodeString()
452 default_password_reset = v.OneOf(password_reset_choices)
454 default_password_reset = v.OneOf(password_reset_choices)
453 default_extern_activate = v.OneOf(extern_activate_choices)
455 default_extern_activate = v.OneOf(extern_activate_choices)
454
456
455 return _DefaultPermissionsForm
457 return _DefaultPermissionsForm
456
458
457
459
458 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
460 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
459 user_group_perms_choices):
461 user_group_perms_choices):
460 class _ObjectPermissionsForm(formencode.Schema):
462 class _ObjectPermissionsForm(formencode.Schema):
461 allow_extra_fields = True
463 allow_extra_fields = True
462 filter_extra_fields = True
464 filter_extra_fields = True
463 overwrite_default_repo = v.StringBoolean(if_missing=False)
465 overwrite_default_repo = v.StringBoolean(if_missing=False)
464 overwrite_default_group = v.StringBoolean(if_missing=False)
466 overwrite_default_group = v.StringBoolean(if_missing=False)
465 overwrite_default_user_group = v.StringBoolean(if_missing=False)
467 overwrite_default_user_group = v.StringBoolean(if_missing=False)
466 default_repo_perm = v.OneOf(repo_perms_choices)
468 default_repo_perm = v.OneOf(repo_perms_choices)
467 default_group_perm = v.OneOf(group_perms_choices)
469 default_group_perm = v.OneOf(group_perms_choices)
468 default_user_group_perm = v.OneOf(user_group_perms_choices)
470 default_user_group_perm = v.OneOf(user_group_perms_choices)
469
471
470 return _ObjectPermissionsForm
472 return _ObjectPermissionsForm
471
473
472
474
473 def UserPermissionsForm(create_choices, create_on_write_choices,
475 def UserPermissionsForm(create_choices, create_on_write_choices,
474 repo_group_create_choices, user_group_create_choices,
476 repo_group_create_choices, user_group_create_choices,
475 fork_choices, inherit_default_permissions_choices):
477 fork_choices, inherit_default_permissions_choices):
476 class _DefaultPermissionsForm(formencode.Schema):
478 class _DefaultPermissionsForm(formencode.Schema):
477 allow_extra_fields = True
479 allow_extra_fields = True
478 filter_extra_fields = True
480 filter_extra_fields = True
479
481
480 anonymous = v.StringBoolean(if_missing=False)
482 anonymous = v.StringBoolean(if_missing=False)
481
483
482 default_repo_create = v.OneOf(create_choices)
484 default_repo_create = v.OneOf(create_choices)
483 default_repo_create_on_write = v.OneOf(create_on_write_choices)
485 default_repo_create_on_write = v.OneOf(create_on_write_choices)
484 default_user_group_create = v.OneOf(user_group_create_choices)
486 default_user_group_create = v.OneOf(user_group_create_choices)
485 default_repo_group_create = v.OneOf(repo_group_create_choices)
487 default_repo_group_create = v.OneOf(repo_group_create_choices)
486 default_fork_create = v.OneOf(fork_choices)
488 default_fork_create = v.OneOf(fork_choices)
487 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
489 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
488
490
489 return _DefaultPermissionsForm
491 return _DefaultPermissionsForm
490
492
491
493
492 def UserIndividualPermissionsForm():
494 def UserIndividualPermissionsForm():
493 class _DefaultPermissionsForm(formencode.Schema):
495 class _DefaultPermissionsForm(formencode.Schema):
494 allow_extra_fields = True
496 allow_extra_fields = True
495 filter_extra_fields = True
497 filter_extra_fields = True
496
498
497 inherit_default_permissions = v.StringBoolean(if_missing=False)
499 inherit_default_permissions = v.StringBoolean(if_missing=False)
498
500
499 return _DefaultPermissionsForm
501 return _DefaultPermissionsForm
500
502
501
503
502 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
504 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
503 class _DefaultsForm(formencode.Schema):
505 class _DefaultsForm(formencode.Schema):
504 allow_extra_fields = True
506 allow_extra_fields = True
505 filter_extra_fields = True
507 filter_extra_fields = True
506 default_repo_type = v.OneOf(supported_backends)
508 default_repo_type = v.OneOf(supported_backends)
507 default_repo_private = v.StringBoolean(if_missing=False)
509 default_repo_private = v.StringBoolean(if_missing=False)
508 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
510 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
509 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
511 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
510 default_repo_enable_locking = v.StringBoolean(if_missing=False)
512 default_repo_enable_locking = v.StringBoolean(if_missing=False)
511
513
512 return _DefaultsForm
514 return _DefaultsForm
513
515
514
516
515 def AuthSettingsForm():
517 def AuthSettingsForm():
516 class _AuthSettingsForm(formencode.Schema):
518 class _AuthSettingsForm(formencode.Schema):
517 allow_extra_fields = True
519 allow_extra_fields = True
518 filter_extra_fields = True
520 filter_extra_fields = True
519 auth_plugins = All(v.ValidAuthPlugins(),
521 auth_plugins = All(v.ValidAuthPlugins(),
520 v.UniqueListFromString()(not_empty=True))
522 v.UniqueListFromString()(not_empty=True))
521
523
522 return _AuthSettingsForm
524 return _AuthSettingsForm
523
525
524
526
525 def UserExtraEmailForm():
527 def UserExtraEmailForm():
526 class _UserExtraEmailForm(formencode.Schema):
528 class _UserExtraEmailForm(formencode.Schema):
527 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
529 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
528 return _UserExtraEmailForm
530 return _UserExtraEmailForm
529
531
530
532
531 def UserExtraIpForm():
533 def UserExtraIpForm():
532 class _UserExtraIpForm(formencode.Schema):
534 class _UserExtraIpForm(formencode.Schema):
533 ip = v.ValidIp()(not_empty=True)
535 ip = v.ValidIp()(not_empty=True)
534 return _UserExtraIpForm
536 return _UserExtraIpForm
535
537
536
538
537
539
538 def PullRequestForm(repo_id):
540 def PullRequestForm(repo_id):
539 class ReviewerForm(formencode.Schema):
541 class ReviewerForm(formencode.Schema):
540 user_id = v.Int(not_empty=True)
542 user_id = v.Int(not_empty=True)
541 reasons = All()
543 reasons = All()
542 mandatory = v.StringBoolean()
544 mandatory = v.StringBoolean()
543
545
544 class _PullRequestForm(formencode.Schema):
546 class _PullRequestForm(formencode.Schema):
545 allow_extra_fields = True
547 allow_extra_fields = True
546 filter_extra_fields = True
548 filter_extra_fields = True
547
549
548 common_ancestor = v.UnicodeString(strip=True, required=True)
550 common_ancestor = v.UnicodeString(strip=True, required=True)
549 source_repo = v.UnicodeString(strip=True, required=True)
551 source_repo = v.UnicodeString(strip=True, required=True)
550 source_ref = v.UnicodeString(strip=True, required=True)
552 source_ref = v.UnicodeString(strip=True, required=True)
551 target_repo = v.UnicodeString(strip=True, required=True)
553 target_repo = v.UnicodeString(strip=True, required=True)
552 target_ref = v.UnicodeString(strip=True, required=True)
554 target_ref = v.UnicodeString(strip=True, required=True)
553 revisions = All(#v.NotReviewedRevisions(repo_id)(),
555 revisions = All(#v.NotReviewedRevisions(repo_id)(),
554 v.UniqueList()(not_empty=True))
556 v.UniqueList()(not_empty=True))
555 review_members = formencode.ForEach(ReviewerForm())
557 review_members = formencode.ForEach(ReviewerForm())
556 pullrequest_title = v.UnicodeString(strip=True, required=True)
558 pullrequest_title = v.UnicodeString(strip=True, required=True)
557 pullrequest_desc = v.UnicodeString(strip=True, required=False)
559 pullrequest_desc = v.UnicodeString(strip=True, required=False)
558
560
559 return _PullRequestForm
561 return _PullRequestForm
560
562
561
563
562 def IssueTrackerPatternsForm():
564 def IssueTrackerPatternsForm():
563 class _IssueTrackerPatternsForm(formencode.Schema):
565 class _IssueTrackerPatternsForm(formencode.Schema):
564 allow_extra_fields = True
566 allow_extra_fields = True
565 filter_extra_fields = False
567 filter_extra_fields = False
566 chained_validators = [v.ValidPattern()]
568 chained_validators = [v.ValidPattern()]
567 return _IssueTrackerPatternsForm
569 return _IssueTrackerPatternsForm
@@ -1,1595 +1,1609 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
78
78
79 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
84 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
89 ' failed.'),
89 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
92 ' head.'),
92 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
95 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
98 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
101 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
104 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
110 'reference is missing.'),
110 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
113 'to sub repositories.'),
114 }
114 }
115
115
116 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
118 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
128 'reference is missing.'),
128 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
131 'reference is missing.'),
131 'reference is missing.'),
132 }
132 }
133
133
134 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
135 return self._get_instance((
135 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
137
137
138 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
139 if not api:
140 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
142 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
145
145
146 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
149
149
150 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
153
153
154 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
157
157
158 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
162
162
163 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
165 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
167
167
168 def get(self, pull_request):
168 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
170
170
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
173 order_dir='desc'):
173 order_dir='desc'):
174 repo = None
174 repo = None
175 if repo_name:
175 if repo_name:
176 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
177
177
178 q = PullRequest.query()
178 q = PullRequest.query()
179
179
180 # source or target
180 # source or target
181 if repo and source:
181 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
183 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
185
185
186 # closed,opened
186 # closed,opened
187 if statuses:
187 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
189
189
190 # opened by filter
190 # opened by filter
191 if opened_by:
191 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
193
193
194 if order_by:
194 if order_by:
195 order_map = {
195 order_map = {
196 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
197 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
200 }
200 }
201 if order_dir == 'asc':
201 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
203 else:
203 else:
204 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
205
205
206 return q
206 return q
207
207
208 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
209 opened_by=None):
210 """
210 """
211 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
212
212
213 :param repo_name: target or source repo
213 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
217 :returns: int number of pull requests
218 """
218 """
219 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
221
222 return q.count()
222 return q.count()
223
223
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
226 """
227 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :param offset: pagination offset
233 :param offset: pagination offset
234 :param length: length of returned list
234 :param length: length of returned list
235 :param order_by: order of the returned list
235 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
237 :returns: list of pull requests
238 """
238 """
239 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242
242
243 if length:
243 if length:
244 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
245 else:
245 else:
246 pull_requests = q.all()
246 pull_requests = q.all()
247
247
248 return pull_requests
248 return pull_requests
249
249
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
251 opened_by=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review.
254 awaiting review.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
260 :returns: int number of pull requests
261 """
261 """
262 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
264
265 return len(pull_requests)
265 return len(pull_requests)
266
266
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
270 """
270 """
271 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
272 review.
272 review.
273
273
274 :param repo_name: target or source repo
274 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
278 :param offset: pagination offset
278 :param offset: pagination offset
279 :param length: length of returned list
279 :param length: length of returned list
280 :param order_by: order of the returned list
280 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
282 :returns: list of pull requests
283 """
283 """
284 pull_requests = self.get_all(
284 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
287
287
288 _filtered_pull_requests = []
288 _filtered_pull_requests = []
289 for pr in pull_requests:
289 for pr in pull_requests:
290 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
294 if length:
294 if length:
295 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
296 else:
296 else:
297 return _filtered_pull_requests
297 return _filtered_pull_requests
298
298
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
301 """
301 """
302 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
303 awaiting review from a specific user.
304
304
305 :param repo_name: target or source repo
305 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
314 user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
330 :param offset: pagination offset
331 :param length: length of returned list
331 :param length: length of returned list
332 :param order_by: order of the returned list
332 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
334 :returns: list of pull requests
335 """
335 """
336 pull_requests = self.get_all(
336 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
339
339
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
341 my_participation = []
342 for pr in pull_requests:
342 for pr in pull_requests:
343 if pr in _my:
343 if pr in _my:
344 my_participation.append(pr)
344 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
346 if length:
346 if length:
347 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
348 else:
348 else:
349 return _filtered_pull_requests
349 return _filtered_pull_requests
350
350
351 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
352 return [
352 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
355 ]
355 ]
356
356
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
359 q = PullRequest.query()
360 if user_id:
360 if user_id:
361 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
364 user_filter= or_(
365 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
367 )
368 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
369
369
370 # closed,opened
370 # closed,opened
371 if statuses:
371 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
373
373
374 if order_by:
374 if order_by:
375 order_map = {
375 order_map = {
376 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
377 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
380 }
380 }
381 if order_dir == 'asc':
381 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
383 else:
383 else:
384 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
385
385
386 return q
386 return q
387
387
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
390 return q.count()
391
391
392 def get_im_participating_in(
392 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
395 """
395 """
396 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
397 """
397 """
398
398
399 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
401 order_dir=order_dir)
402
402
403 if length:
403 if length:
404 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
405 else:
405 else:
406 pull_requests = q.all()
406 pull_requests = q.all()
407
407
408 return pull_requests
408 return pull_requests
409
409
410 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
411 """
411 """
412 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
413 """
413 """
414 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
417 .all()
418
418
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
421 reviewer_data=None):
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 self.notify_reviewers(pull_request, reviewer_ids)
469 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
476 created_by_user, pull_request)
477
477
478 return pull_request
478 return pull_request
479
479
480 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 pull_request = self.__get_pull_request(pull_request)
481 pull_request = self.__get_pull_request(pull_request)
482 target_scm = pull_request.target_repo.scm_instance()
482 target_scm = pull_request.target_repo.scm_instance()
483 if action == 'create':
483 if action == 'create':
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 elif action == 'merge':
485 elif action == 'merge':
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 elif action == 'close':
487 elif action == 'close':
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 elif action == 'review_status_change':
489 elif action == 'review_status_change':
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 elif action == 'update':
491 elif action == 'update':
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 else:
493 else:
494 return
494 return
495
495
496 trigger_hook(
496 trigger_hook(
497 username=user.username,
497 username=user.username,
498 repo_name=pull_request.target_repo.repo_name,
498 repo_name=pull_request.target_repo.repo_name,
499 repo_alias=target_scm.alias,
499 repo_alias=target_scm.alias,
500 pull_request=pull_request)
500 pull_request=pull_request)
501
501
502 def _get_commit_ids(self, pull_request):
502 def _get_commit_ids(self, pull_request):
503 """
503 """
504 Return the commit ids of the merged pull request.
504 Return the commit ids of the merged pull request.
505
505
506 This method is not dealing correctly yet with the lack of autoupdates
506 This method is not dealing correctly yet with the lack of autoupdates
507 nor with the implicit target updates.
507 nor with the implicit target updates.
508 For example: if a commit in the source repo is already in the target it
508 For example: if a commit in the source repo is already in the target it
509 will be reported anyways.
509 will be reported anyways.
510 """
510 """
511 merge_rev = pull_request.merge_rev
511 merge_rev = pull_request.merge_rev
512 if merge_rev is None:
512 if merge_rev is None:
513 raise ValueError('This pull request was not merged yet')
513 raise ValueError('This pull request was not merged yet')
514
514
515 commit_ids = list(pull_request.revisions)
515 commit_ids = list(pull_request.revisions)
516 if merge_rev not in commit_ids:
516 if merge_rev not in commit_ids:
517 commit_ids.append(merge_rev)
517 commit_ids.append(merge_rev)
518
518
519 return commit_ids
519 return commit_ids
520
520
521 def merge(self, pull_request, user, extras):
521 def merge(self, pull_request, user, extras):
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 if merge_state.executed:
524 if merge_state.executed:
525 log.debug(
525 log.debug(
526 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
527 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
528
528
529 self._log_audit_action(
529 self._log_audit_action(
530 'repo.pull_request.merge',
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
532 user, pull_request)
533
533
534 else:
534 else:
535 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
536 return merge_state
537
537
538 def _merge_pull_request(self, pull_request, user, extras):
538 def _merge_pull_request(self, pull_request, user, extras):
539 target_vcs = pull_request.target_repo.scm_instance()
539 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
541 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
542 pull_request.target_ref_parts, target_vcs)
543
543
544 message = _(
544 message = _(
545 'Merge pull request #%(pr_id)s from '
545 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
547 'pr_id': pull_request.pull_request_id,
548 'source_repo': source_vcs.name,
548 'source_repo': source_vcs.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
550 'pr_title': pull_request.title
550 'pr_title': pull_request.title
551 }
551 }
552
552
553 workspace_id = self._workspace_id(pull_request)
553 workspace_id = self._workspace_id(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
555 close_branch = self._close_branch_before_merging(pull_request)
555 close_branch = self._close_branch_before_merging(pull_request)
556
556
557 callback_daemon, extras = prepare_callback_daemon(
557 callback_daemon, extras = prepare_callback_daemon(
558 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
559 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
560
560
561 with callback_daemon:
561 with callback_daemon:
562 # TODO: johbo: Implement a clean way to run a config_override
562 # TODO: johbo: Implement a clean way to run a config_override
563 # for a single call.
563 # for a single call.
564 target_vcs.config.set(
564 target_vcs.config.set(
565 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
566 merge_state = target_vcs.merge(
566 merge_state = target_vcs.merge(
567 target_ref, source_vcs, pull_request.source_ref_parts,
567 target_ref, source_vcs, pull_request.source_ref_parts,
568 workspace_id, user_name=user.username,
568 workspace_id, user_name=user.username,
569 user_email=user.email, message=message, use_rebase=use_rebase,
569 user_email=user.email, message=message, use_rebase=use_rebase,
570 close_branch=close_branch)
570 close_branch=close_branch)
571 return merge_state
571 return merge_state
572
572
573 def _comment_and_close_pr(self, pull_request, user, merge_state):
573 def _comment_and_close_pr(self, pull_request, user, merge_state):
574 pull_request.merge_rev = merge_state.merge_ref.commit_id
574 pull_request.merge_rev = merge_state.merge_ref.commit_id
575 pull_request.updated_on = datetime.datetime.now()
575 pull_request.updated_on = datetime.datetime.now()
576
576
577 CommentsModel().create(
577 CommentsModel().create(
578 text=unicode(_('Pull request merged and closed')),
578 text=unicode(_('Pull request merged and closed')),
579 repo=pull_request.target_repo.repo_id,
579 repo=pull_request.target_repo.repo_id,
580 user=user.user_id,
580 user=user.user_id,
581 pull_request=pull_request.pull_request_id,
581 pull_request=pull_request.pull_request_id,
582 f_path=None,
582 f_path=None,
583 line_no=None,
583 line_no=None,
584 closing_pr=True
584 closing_pr=True
585 )
585 )
586
586
587 Session().add(pull_request)
587 Session().add(pull_request)
588 Session().flush()
588 Session().flush()
589 # TODO: paris: replace invalidation with less radical solution
589 # TODO: paris: replace invalidation with less radical solution
590 ScmModel().mark_for_invalidation(
590 ScmModel().mark_for_invalidation(
591 pull_request.target_repo.repo_name)
591 pull_request.target_repo.repo_name)
592 self._trigger_pull_request_hook(pull_request, user, 'merge')
592 self._trigger_pull_request_hook(pull_request, user, 'merge')
593
593
594 def has_valid_update_type(self, pull_request):
594 def has_valid_update_type(self, pull_request):
595 source_ref_type = pull_request.source_ref_parts.type
595 source_ref_type = pull_request.source_ref_parts.type
596 return source_ref_type in ['book', 'branch', 'tag']
596 return source_ref_type in ['book', 'branch', 'tag']
597
597
598 def update_commits(self, pull_request):
598 def update_commits(self, pull_request):
599 """
599 """
600 Get the updated list of commits for the pull request
600 Get the updated list of commits for the pull request
601 and return the new pull request version and the list
601 and return the new pull request version and the list
602 of commits processed by this update action
602 of commits processed by this update action
603 """
603 """
604 pull_request = self.__get_pull_request(pull_request)
604 pull_request = self.__get_pull_request(pull_request)
605 source_ref_type = pull_request.source_ref_parts.type
605 source_ref_type = pull_request.source_ref_parts.type
606 source_ref_name = pull_request.source_ref_parts.name
606 source_ref_name = pull_request.source_ref_parts.name
607 source_ref_id = pull_request.source_ref_parts.commit_id
607 source_ref_id = pull_request.source_ref_parts.commit_id
608
608
609 target_ref_type = pull_request.target_ref_parts.type
609 target_ref_type = pull_request.target_ref_parts.type
610 target_ref_name = pull_request.target_ref_parts.name
610 target_ref_name = pull_request.target_ref_parts.name
611 target_ref_id = pull_request.target_ref_parts.commit_id
611 target_ref_id = pull_request.target_ref_parts.commit_id
612
612
613 if not self.has_valid_update_type(pull_request):
613 if not self.has_valid_update_type(pull_request):
614 log.debug(
614 log.debug(
615 "Skipping update of pull request %s due to ref type: %s",
615 "Skipping update of pull request %s due to ref type: %s",
616 pull_request, source_ref_type)
616 pull_request, source_ref_type)
617 return UpdateResponse(
617 return UpdateResponse(
618 executed=False,
618 executed=False,
619 reason=UpdateFailureReason.WRONG_REF_TYPE,
619 reason=UpdateFailureReason.WRONG_REF_TYPE,
620 old=pull_request, new=None, changes=None,
620 old=pull_request, new=None, changes=None,
621 source_changed=False, target_changed=False)
621 source_changed=False, target_changed=False)
622
622
623 # source repo
623 # source repo
624 source_repo = pull_request.source_repo.scm_instance()
624 source_repo = pull_request.source_repo.scm_instance()
625 try:
625 try:
626 source_commit = source_repo.get_commit(commit_id=source_ref_name)
626 source_commit = source_repo.get_commit(commit_id=source_ref_name)
627 except CommitDoesNotExistError:
627 except CommitDoesNotExistError:
628 return UpdateResponse(
628 return UpdateResponse(
629 executed=False,
629 executed=False,
630 reason=UpdateFailureReason.MISSING_SOURCE_REF,
630 reason=UpdateFailureReason.MISSING_SOURCE_REF,
631 old=pull_request, new=None, changes=None,
631 old=pull_request, new=None, changes=None,
632 source_changed=False, target_changed=False)
632 source_changed=False, target_changed=False)
633
633
634 source_changed = source_ref_id != source_commit.raw_id
634 source_changed = source_ref_id != source_commit.raw_id
635
635
636 # target repo
636 # target repo
637 target_repo = pull_request.target_repo.scm_instance()
637 target_repo = pull_request.target_repo.scm_instance()
638 try:
638 try:
639 target_commit = target_repo.get_commit(commit_id=target_ref_name)
639 target_commit = target_repo.get_commit(commit_id=target_ref_name)
640 except CommitDoesNotExistError:
640 except CommitDoesNotExistError:
641 return UpdateResponse(
641 return UpdateResponse(
642 executed=False,
642 executed=False,
643 reason=UpdateFailureReason.MISSING_TARGET_REF,
643 reason=UpdateFailureReason.MISSING_TARGET_REF,
644 old=pull_request, new=None, changes=None,
644 old=pull_request, new=None, changes=None,
645 source_changed=False, target_changed=False)
645 source_changed=False, target_changed=False)
646 target_changed = target_ref_id != target_commit.raw_id
646 target_changed = target_ref_id != target_commit.raw_id
647
647
648 if not (source_changed or target_changed):
648 if not (source_changed or target_changed):
649 log.debug("Nothing changed in pull request %s", pull_request)
649 log.debug("Nothing changed in pull request %s", pull_request)
650 return UpdateResponse(
650 return UpdateResponse(
651 executed=False,
651 executed=False,
652 reason=UpdateFailureReason.NO_CHANGE,
652 reason=UpdateFailureReason.NO_CHANGE,
653 old=pull_request, new=None, changes=None,
653 old=pull_request, new=None, changes=None,
654 source_changed=target_changed, target_changed=source_changed)
654 source_changed=target_changed, target_changed=source_changed)
655
655
656 change_in_found = 'target repo' if target_changed else 'source repo'
656 change_in_found = 'target repo' if target_changed else 'source repo'
657 log.debug('Updating pull request because of change in %s detected',
657 log.debug('Updating pull request because of change in %s detected',
658 change_in_found)
658 change_in_found)
659
659
660 # Finally there is a need for an update, in case of source change
660 # Finally there is a need for an update, in case of source change
661 # we create a new version, else just an update
661 # we create a new version, else just an update
662 if source_changed:
662 if source_changed:
663 pull_request_version = self._create_version_from_snapshot(pull_request)
663 pull_request_version = self._create_version_from_snapshot(pull_request)
664 self._link_comments_to_version(pull_request_version)
664 self._link_comments_to_version(pull_request_version)
665 else:
665 else:
666 try:
666 try:
667 ver = pull_request.versions[-1]
667 ver = pull_request.versions[-1]
668 except IndexError:
668 except IndexError:
669 ver = None
669 ver = None
670
670
671 pull_request.pull_request_version_id = \
671 pull_request.pull_request_version_id = \
672 ver.pull_request_version_id if ver else None
672 ver.pull_request_version_id if ver else None
673 pull_request_version = pull_request
673 pull_request_version = pull_request
674
674
675 try:
675 try:
676 if target_ref_type in ('tag', 'branch', 'book'):
676 if target_ref_type in ('tag', 'branch', 'book'):
677 target_commit = target_repo.get_commit(target_ref_name)
677 target_commit = target_repo.get_commit(target_ref_name)
678 else:
678 else:
679 target_commit = target_repo.get_commit(target_ref_id)
679 target_commit = target_repo.get_commit(target_ref_id)
680 except CommitDoesNotExistError:
680 except CommitDoesNotExistError:
681 return UpdateResponse(
681 return UpdateResponse(
682 executed=False,
682 executed=False,
683 reason=UpdateFailureReason.MISSING_TARGET_REF,
683 reason=UpdateFailureReason.MISSING_TARGET_REF,
684 old=pull_request, new=None, changes=None,
684 old=pull_request, new=None, changes=None,
685 source_changed=source_changed, target_changed=target_changed)
685 source_changed=source_changed, target_changed=target_changed)
686
686
687 # re-compute commit ids
687 # re-compute commit ids
688 old_commit_ids = pull_request.revisions
688 old_commit_ids = pull_request.revisions
689 pre_load = ["author", "branch", "date", "message"]
689 pre_load = ["author", "branch", "date", "message"]
690 commit_ranges = target_repo.compare(
690 commit_ranges = target_repo.compare(
691 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
691 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
692 pre_load=pre_load)
692 pre_load=pre_load)
693
693
694 ancestor = target_repo.get_common_ancestor(
694 ancestor = target_repo.get_common_ancestor(
695 target_commit.raw_id, source_commit.raw_id, source_repo)
695 target_commit.raw_id, source_commit.raw_id, source_repo)
696
696
697 pull_request.source_ref = '%s:%s:%s' % (
697 pull_request.source_ref = '%s:%s:%s' % (
698 source_ref_type, source_ref_name, source_commit.raw_id)
698 source_ref_type, source_ref_name, source_commit.raw_id)
699 pull_request.target_ref = '%s:%s:%s' % (
699 pull_request.target_ref = '%s:%s:%s' % (
700 target_ref_type, target_ref_name, ancestor)
700 target_ref_type, target_ref_name, ancestor)
701
701
702 pull_request.revisions = [
702 pull_request.revisions = [
703 commit.raw_id for commit in reversed(commit_ranges)]
703 commit.raw_id for commit in reversed(commit_ranges)]
704 pull_request.updated_on = datetime.datetime.now()
704 pull_request.updated_on = datetime.datetime.now()
705 Session().add(pull_request)
705 Session().add(pull_request)
706 new_commit_ids = pull_request.revisions
706 new_commit_ids = pull_request.revisions
707
707
708 old_diff_data, new_diff_data = self._generate_update_diffs(
708 old_diff_data, new_diff_data = self._generate_update_diffs(
709 pull_request, pull_request_version)
709 pull_request, pull_request_version)
710
710
711 # calculate commit and file changes
711 # calculate commit and file changes
712 changes = self._calculate_commit_id_changes(
712 changes = self._calculate_commit_id_changes(
713 old_commit_ids, new_commit_ids)
713 old_commit_ids, new_commit_ids)
714 file_changes = self._calculate_file_changes(
714 file_changes = self._calculate_file_changes(
715 old_diff_data, new_diff_data)
715 old_diff_data, new_diff_data)
716
716
717 # set comments as outdated if DIFFS changed
717 # set comments as outdated if DIFFS changed
718 CommentsModel().outdate_comments(
718 CommentsModel().outdate_comments(
719 pull_request, old_diff_data=old_diff_data,
719 pull_request, old_diff_data=old_diff_data,
720 new_diff_data=new_diff_data)
720 new_diff_data=new_diff_data)
721
721
722 commit_changes = (changes.added or changes.removed)
722 commit_changes = (changes.added or changes.removed)
723 file_node_changes = (
723 file_node_changes = (
724 file_changes.added or file_changes.modified or file_changes.removed)
724 file_changes.added or file_changes.modified or file_changes.removed)
725 pr_has_changes = commit_changes or file_node_changes
725 pr_has_changes = commit_changes or file_node_changes
726
726
727 # Add an automatic comment to the pull request, in case
727 # Add an automatic comment to the pull request, in case
728 # anything has changed
728 # anything has changed
729 if pr_has_changes:
729 if pr_has_changes:
730 update_comment = CommentsModel().create(
730 update_comment = CommentsModel().create(
731 text=self._render_update_message(changes, file_changes),
731 text=self._render_update_message(changes, file_changes),
732 repo=pull_request.target_repo,
732 repo=pull_request.target_repo,
733 user=pull_request.author,
733 user=pull_request.author,
734 pull_request=pull_request,
734 pull_request=pull_request,
735 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
735 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
736
736
737 # Update status to "Under Review" for added commits
737 # Update status to "Under Review" for added commits
738 for commit_id in changes.added:
738 for commit_id in changes.added:
739 ChangesetStatusModel().set_status(
739 ChangesetStatusModel().set_status(
740 repo=pull_request.source_repo,
740 repo=pull_request.source_repo,
741 status=ChangesetStatus.STATUS_UNDER_REVIEW,
741 status=ChangesetStatus.STATUS_UNDER_REVIEW,
742 comment=update_comment,
742 comment=update_comment,
743 user=pull_request.author,
743 user=pull_request.author,
744 pull_request=pull_request,
744 pull_request=pull_request,
745 revision=commit_id)
745 revision=commit_id)
746
746
747 log.debug(
747 log.debug(
748 'Updated pull request %s, added_ids: %s, common_ids: %s, '
748 'Updated pull request %s, added_ids: %s, common_ids: %s, '
749 'removed_ids: %s', pull_request.pull_request_id,
749 'removed_ids: %s', pull_request.pull_request_id,
750 changes.added, changes.common, changes.removed)
750 changes.added, changes.common, changes.removed)
751 log.debug(
751 log.debug(
752 'Updated pull request with the following file changes: %s',
752 'Updated pull request with the following file changes: %s',
753 file_changes)
753 file_changes)
754
754
755 log.info(
755 log.info(
756 "Updated pull request %s from commit %s to commit %s, "
756 "Updated pull request %s from commit %s to commit %s, "
757 "stored new version %s of this pull request.",
757 "stored new version %s of this pull request.",
758 pull_request.pull_request_id, source_ref_id,
758 pull_request.pull_request_id, source_ref_id,
759 pull_request.source_ref_parts.commit_id,
759 pull_request.source_ref_parts.commit_id,
760 pull_request_version.pull_request_version_id)
760 pull_request_version.pull_request_version_id)
761 Session().commit()
761 Session().commit()
762 self._trigger_pull_request_hook(
762 self._trigger_pull_request_hook(
763 pull_request, pull_request.author, 'update')
763 pull_request, pull_request.author, 'update')
764
764
765 return UpdateResponse(
765 return UpdateResponse(
766 executed=True, reason=UpdateFailureReason.NONE,
766 executed=True, reason=UpdateFailureReason.NONE,
767 old=pull_request, new=pull_request_version, changes=changes,
767 old=pull_request, new=pull_request_version, changes=changes,
768 source_changed=source_changed, target_changed=target_changed)
768 source_changed=source_changed, target_changed=target_changed)
769
769
770 def _create_version_from_snapshot(self, pull_request):
770 def _create_version_from_snapshot(self, pull_request):
771 version = PullRequestVersion()
771 version = PullRequestVersion()
772 version.title = pull_request.title
772 version.title = pull_request.title
773 version.description = pull_request.description
773 version.description = pull_request.description
774 version.status = pull_request.status
774 version.status = pull_request.status
775 version.created_on = datetime.datetime.now()
775 version.created_on = datetime.datetime.now()
776 version.updated_on = pull_request.updated_on
776 version.updated_on = pull_request.updated_on
777 version.user_id = pull_request.user_id
777 version.user_id = pull_request.user_id
778 version.source_repo = pull_request.source_repo
778 version.source_repo = pull_request.source_repo
779 version.source_ref = pull_request.source_ref
779 version.source_ref = pull_request.source_ref
780 version.target_repo = pull_request.target_repo
780 version.target_repo = pull_request.target_repo
781 version.target_ref = pull_request.target_ref
781 version.target_ref = pull_request.target_ref
782
782
783 version._last_merge_source_rev = pull_request._last_merge_source_rev
783 version._last_merge_source_rev = pull_request._last_merge_source_rev
784 version._last_merge_target_rev = pull_request._last_merge_target_rev
784 version._last_merge_target_rev = pull_request._last_merge_target_rev
785 version.last_merge_status = pull_request.last_merge_status
785 version.last_merge_status = pull_request.last_merge_status
786 version.shadow_merge_ref = pull_request.shadow_merge_ref
786 version.shadow_merge_ref = pull_request.shadow_merge_ref
787 version.merge_rev = pull_request.merge_rev
787 version.merge_rev = pull_request.merge_rev
788 version.reviewer_data = pull_request.reviewer_data
788 version.reviewer_data = pull_request.reviewer_data
789
789
790 version.revisions = pull_request.revisions
790 version.revisions = pull_request.revisions
791 version.pull_request = pull_request
791 version.pull_request = pull_request
792 Session().add(version)
792 Session().add(version)
793 Session().flush()
793 Session().flush()
794
794
795 return version
795 return version
796
796
797 def _generate_update_diffs(self, pull_request, pull_request_version):
797 def _generate_update_diffs(self, pull_request, pull_request_version):
798
798
799 diff_context = (
799 diff_context = (
800 self.DIFF_CONTEXT +
800 self.DIFF_CONTEXT +
801 CommentsModel.needed_extra_diff_context())
801 CommentsModel.needed_extra_diff_context())
802
802
803 source_repo = pull_request_version.source_repo
803 source_repo = pull_request_version.source_repo
804 source_ref_id = pull_request_version.source_ref_parts.commit_id
804 source_ref_id = pull_request_version.source_ref_parts.commit_id
805 target_ref_id = pull_request_version.target_ref_parts.commit_id
805 target_ref_id = pull_request_version.target_ref_parts.commit_id
806 old_diff = self._get_diff_from_pr_or_version(
806 old_diff = self._get_diff_from_pr_or_version(
807 source_repo, source_ref_id, target_ref_id, context=diff_context)
807 source_repo, source_ref_id, target_ref_id, context=diff_context)
808
808
809 source_repo = pull_request.source_repo
809 source_repo = pull_request.source_repo
810 source_ref_id = pull_request.source_ref_parts.commit_id
810 source_ref_id = pull_request.source_ref_parts.commit_id
811 target_ref_id = pull_request.target_ref_parts.commit_id
811 target_ref_id = pull_request.target_ref_parts.commit_id
812
812
813 new_diff = self._get_diff_from_pr_or_version(
813 new_diff = self._get_diff_from_pr_or_version(
814 source_repo, source_ref_id, target_ref_id, context=diff_context)
814 source_repo, source_ref_id, target_ref_id, context=diff_context)
815
815
816 old_diff_data = diffs.DiffProcessor(old_diff)
816 old_diff_data = diffs.DiffProcessor(old_diff)
817 old_diff_data.prepare()
817 old_diff_data.prepare()
818 new_diff_data = diffs.DiffProcessor(new_diff)
818 new_diff_data = diffs.DiffProcessor(new_diff)
819 new_diff_data.prepare()
819 new_diff_data.prepare()
820
820
821 return old_diff_data, new_diff_data
821 return old_diff_data, new_diff_data
822
822
823 def _link_comments_to_version(self, pull_request_version):
823 def _link_comments_to_version(self, pull_request_version):
824 """
824 """
825 Link all unlinked comments of this pull request to the given version.
825 Link all unlinked comments of this pull request to the given version.
826
826
827 :param pull_request_version: The `PullRequestVersion` to which
827 :param pull_request_version: The `PullRequestVersion` to which
828 the comments shall be linked.
828 the comments shall be linked.
829
829
830 """
830 """
831 pull_request = pull_request_version.pull_request
831 pull_request = pull_request_version.pull_request
832 comments = ChangesetComment.query()\
832 comments = ChangesetComment.query()\
833 .filter(
833 .filter(
834 # TODO: johbo: Should we query for the repo at all here?
834 # TODO: johbo: Should we query for the repo at all here?
835 # Pending decision on how comments of PRs are to be related
835 # Pending decision on how comments of PRs are to be related
836 # to either the source repo, the target repo or no repo at all.
836 # to either the source repo, the target repo or no repo at all.
837 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
837 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
838 ChangesetComment.pull_request == pull_request,
838 ChangesetComment.pull_request == pull_request,
839 ChangesetComment.pull_request_version == None)\
839 ChangesetComment.pull_request_version == None)\
840 .order_by(ChangesetComment.comment_id.asc())
840 .order_by(ChangesetComment.comment_id.asc())
841
841
842 # TODO: johbo: Find out why this breaks if it is done in a bulk
842 # TODO: johbo: Find out why this breaks if it is done in a bulk
843 # operation.
843 # operation.
844 for comment in comments:
844 for comment in comments:
845 comment.pull_request_version_id = (
845 comment.pull_request_version_id = (
846 pull_request_version.pull_request_version_id)
846 pull_request_version.pull_request_version_id)
847 Session().add(comment)
847 Session().add(comment)
848
848
849 def _calculate_commit_id_changes(self, old_ids, new_ids):
849 def _calculate_commit_id_changes(self, old_ids, new_ids):
850 added = [x for x in new_ids if x not in old_ids]
850 added = [x for x in new_ids if x not in old_ids]
851 common = [x for x in new_ids if x in old_ids]
851 common = [x for x in new_ids if x in old_ids]
852 removed = [x for x in old_ids if x not in new_ids]
852 removed = [x for x in old_ids if x not in new_ids]
853 total = new_ids
853 total = new_ids
854 return ChangeTuple(added, common, removed, total)
854 return ChangeTuple(added, common, removed, total)
855
855
856 def _calculate_file_changes(self, old_diff_data, new_diff_data):
856 def _calculate_file_changes(self, old_diff_data, new_diff_data):
857
857
858 old_files = OrderedDict()
858 old_files = OrderedDict()
859 for diff_data in old_diff_data.parsed_diff:
859 for diff_data in old_diff_data.parsed_diff:
860 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
860 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
861
861
862 added_files = []
862 added_files = []
863 modified_files = []
863 modified_files = []
864 removed_files = []
864 removed_files = []
865 for diff_data in new_diff_data.parsed_diff:
865 for diff_data in new_diff_data.parsed_diff:
866 new_filename = diff_data['filename']
866 new_filename = diff_data['filename']
867 new_hash = md5_safe(diff_data['raw_diff'])
867 new_hash = md5_safe(diff_data['raw_diff'])
868
868
869 old_hash = old_files.get(new_filename)
869 old_hash = old_files.get(new_filename)
870 if not old_hash:
870 if not old_hash:
871 # file is not present in old diff, means it's added
871 # file is not present in old diff, means it's added
872 added_files.append(new_filename)
872 added_files.append(new_filename)
873 else:
873 else:
874 if new_hash != old_hash:
874 if new_hash != old_hash:
875 modified_files.append(new_filename)
875 modified_files.append(new_filename)
876 # now remove a file from old, since we have seen it already
876 # now remove a file from old, since we have seen it already
877 del old_files[new_filename]
877 del old_files[new_filename]
878
878
879 # removed files is when there are present in old, but not in NEW,
879 # removed files is when there are present in old, but not in NEW,
880 # since we remove old files that are present in new diff, left-overs
880 # since we remove old files that are present in new diff, left-overs
881 # if any should be the removed files
881 # if any should be the removed files
882 removed_files.extend(old_files.keys())
882 removed_files.extend(old_files.keys())
883
883
884 return FileChangeTuple(added_files, modified_files, removed_files)
884 return FileChangeTuple(added_files, modified_files, removed_files)
885
885
886 def _render_update_message(self, changes, file_changes):
886 def _render_update_message(self, changes, file_changes):
887 """
887 """
888 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
888 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
889 so it's always looking the same disregarding on which default
889 so it's always looking the same disregarding on which default
890 renderer system is using.
890 renderer system is using.
891
891
892 :param changes: changes named tuple
892 :param changes: changes named tuple
893 :param file_changes: file changes named tuple
893 :param file_changes: file changes named tuple
894
894
895 """
895 """
896 new_status = ChangesetStatus.get_status_lbl(
896 new_status = ChangesetStatus.get_status_lbl(
897 ChangesetStatus.STATUS_UNDER_REVIEW)
897 ChangesetStatus.STATUS_UNDER_REVIEW)
898
898
899 changed_files = (
899 changed_files = (
900 file_changes.added + file_changes.modified + file_changes.removed)
900 file_changes.added + file_changes.modified + file_changes.removed)
901
901
902 params = {
902 params = {
903 'under_review_label': new_status,
903 'under_review_label': new_status,
904 'added_commits': changes.added,
904 'added_commits': changes.added,
905 'removed_commits': changes.removed,
905 'removed_commits': changes.removed,
906 'changed_files': changed_files,
906 'changed_files': changed_files,
907 'added_files': file_changes.added,
907 'added_files': file_changes.added,
908 'modified_files': file_changes.modified,
908 'modified_files': file_changes.modified,
909 'removed_files': file_changes.removed,
909 'removed_files': file_changes.removed,
910 }
910 }
911 renderer = RstTemplateRenderer()
911 renderer = RstTemplateRenderer()
912 return renderer.render('pull_request_update.mako', **params)
912 return renderer.render('pull_request_update.mako', **params)
913
913
914 def edit(self, pull_request, title, description, user):
914 def edit(self, pull_request, title, description, user):
915 pull_request = self.__get_pull_request(pull_request)
915 pull_request = self.__get_pull_request(pull_request)
916 old_data = pull_request.get_api_data(with_merge_state=False)
916 old_data = pull_request.get_api_data(with_merge_state=False)
917 if pull_request.is_closed():
917 if pull_request.is_closed():
918 raise ValueError('This pull request is closed')
918 raise ValueError('This pull request is closed')
919 if title:
919 if title:
920 pull_request.title = title
920 pull_request.title = title
921 pull_request.description = description
921 pull_request.description = description
922 pull_request.updated_on = datetime.datetime.now()
922 pull_request.updated_on = datetime.datetime.now()
923 Session().add(pull_request)
923 Session().add(pull_request)
924 self._log_audit_action(
924 self._log_audit_action(
925 'repo.pull_request.edit', {'old_data': old_data},
925 'repo.pull_request.edit', {'old_data': old_data},
926 user, pull_request)
926 user, pull_request)
927
927
928 def update_reviewers(self, pull_request, reviewer_data, user):
928 def update_reviewers(self, pull_request, reviewer_data, user):
929 """
929 """
930 Update the reviewers in the pull request
930 Update the reviewers in the pull request
931
931
932 :param pull_request: the pr to update
932 :param pull_request: the pr to update
933 :param reviewer_data: list of tuples
933 :param reviewer_data: list of tuples
934 [(user, ['reason1', 'reason2'], mandatory_flag)]
934 [(user, ['reason1', 'reason2'], mandatory_flag)]
935 """
935 """
936
936
937 reviewers = {}
937 reviewers = {}
938 for user_id, reasons, mandatory in reviewer_data:
938 for user_id, reasons, mandatory in reviewer_data:
939 if isinstance(user_id, (int, basestring)):
939 if isinstance(user_id, (int, basestring)):
940 user_id = self._get_user(user_id).user_id
940 user_id = self._get_user(user_id).user_id
941 reviewers[user_id] = {
941 reviewers[user_id] = {
942 'reasons': reasons, 'mandatory': mandatory}
942 'reasons': reasons, 'mandatory': mandatory}
943
943
944 reviewers_ids = set(reviewers.keys())
944 reviewers_ids = set(reviewers.keys())
945 pull_request = self.__get_pull_request(pull_request)
945 pull_request = self.__get_pull_request(pull_request)
946 current_reviewers = PullRequestReviewers.query()\
946 current_reviewers = PullRequestReviewers.query()\
947 .filter(PullRequestReviewers.pull_request ==
947 .filter(PullRequestReviewers.pull_request ==
948 pull_request).all()
948 pull_request).all()
949 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
949 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
950
950
951 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
951 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
952 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
952 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
953
953
954 log.debug("Adding %s reviewers", ids_to_add)
954 log.debug("Adding %s reviewers", ids_to_add)
955 log.debug("Removing %s reviewers", ids_to_remove)
955 log.debug("Removing %s reviewers", ids_to_remove)
956 changed = False
956 changed = False
957 for uid in ids_to_add:
957 for uid in ids_to_add:
958 changed = True
958 changed = True
959 _usr = self._get_user(uid)
959 _usr = self._get_user(uid)
960 reviewer = PullRequestReviewers()
960 reviewer = PullRequestReviewers()
961 reviewer.user = _usr
961 reviewer.user = _usr
962 reviewer.pull_request = pull_request
962 reviewer.pull_request = pull_request
963 reviewer.reasons = reviewers[uid]['reasons']
963 reviewer.reasons = reviewers[uid]['reasons']
964 # NOTE(marcink): mandatory shouldn't be changed now
964 # NOTE(marcink): mandatory shouldn't be changed now
965 # reviewer.mandatory = reviewers[uid]['reasons']
965 # reviewer.mandatory = reviewers[uid]['reasons']
966 Session().add(reviewer)
966 Session().add(reviewer)
967 self._log_audit_action(
967 self._log_audit_action(
968 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
968 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
969 user, pull_request)
969 user, pull_request)
970
970
971 for uid in ids_to_remove:
971 for uid in ids_to_remove:
972 changed = True
972 changed = True
973 reviewers = PullRequestReviewers.query()\
973 reviewers = PullRequestReviewers.query()\
974 .filter(PullRequestReviewers.user_id == uid,
974 .filter(PullRequestReviewers.user_id == uid,
975 PullRequestReviewers.pull_request == pull_request)\
975 PullRequestReviewers.pull_request == pull_request)\
976 .all()
976 .all()
977 # use .all() in case we accidentally added the same person twice
977 # use .all() in case we accidentally added the same person twice
978 # this CAN happen due to the lack of DB checks
978 # this CAN happen due to the lack of DB checks
979 for obj in reviewers:
979 for obj in reviewers:
980 old_data = obj.get_dict()
980 old_data = obj.get_dict()
981 Session().delete(obj)
981 Session().delete(obj)
982 self._log_audit_action(
982 self._log_audit_action(
983 'repo.pull_request.reviewer.delete',
983 'repo.pull_request.reviewer.delete',
984 {'old_data': old_data}, user, pull_request)
984 {'old_data': old_data}, user, pull_request)
985
985
986 if changed:
986 if changed:
987 pull_request.updated_on = datetime.datetime.now()
987 pull_request.updated_on = datetime.datetime.now()
988 Session().add(pull_request)
988 Session().add(pull_request)
989
989
990 self.notify_reviewers(pull_request, ids_to_add)
990 self.notify_reviewers(pull_request, ids_to_add)
991 return ids_to_add, ids_to_remove
991 return ids_to_add, ids_to_remove
992
992
993 def get_url(self, pull_request, request=None, permalink=False):
993 def get_url(self, pull_request, request=None, permalink=False):
994 if not request:
994 if not request:
995 request = get_current_request()
995 request = get_current_request()
996
996
997 if permalink:
997 if permalink:
998 return request.route_url(
998 return request.route_url(
999 'pull_requests_global',
999 'pull_requests_global',
1000 pull_request_id=pull_request.pull_request_id,)
1000 pull_request_id=pull_request.pull_request_id,)
1001 else:
1001 else:
1002 return request.route_url('pullrequest_show',
1002 return request.route_url('pullrequest_show',
1003 repo_name=safe_str(pull_request.target_repo.repo_name),
1003 repo_name=safe_str(pull_request.target_repo.repo_name),
1004 pull_request_id=pull_request.pull_request_id,)
1004 pull_request_id=pull_request.pull_request_id,)
1005
1005
1006 def get_shadow_clone_url(self, pull_request):
1006 def get_shadow_clone_url(self, pull_request):
1007 """
1007 """
1008 Returns qualified url pointing to the shadow repository. If this pull
1008 Returns qualified url pointing to the shadow repository. If this pull
1009 request is closed there is no shadow repository and ``None`` will be
1009 request is closed there is no shadow repository and ``None`` will be
1010 returned.
1010 returned.
1011 """
1011 """
1012 if pull_request.is_closed():
1012 if pull_request.is_closed():
1013 return None
1013 return None
1014 else:
1014 else:
1015 pr_url = urllib.unquote(self.get_url(pull_request))
1015 pr_url = urllib.unquote(self.get_url(pull_request))
1016 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1016 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1017
1017
1018 def notify_reviewers(self, pull_request, reviewers_ids):
1018 def notify_reviewers(self, pull_request, reviewers_ids):
1019 # notification to reviewers
1019 # notification to reviewers
1020 if not reviewers_ids:
1020 if not reviewers_ids:
1021 return
1021 return
1022
1022
1023 pull_request_obj = pull_request
1023 pull_request_obj = pull_request
1024 # get the current participants of this pull request
1024 # get the current participants of this pull request
1025 recipients = reviewers_ids
1025 recipients = reviewers_ids
1026 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1026 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1027
1027
1028 pr_source_repo = pull_request_obj.source_repo
1028 pr_source_repo = pull_request_obj.source_repo
1029 pr_target_repo = pull_request_obj.target_repo
1029 pr_target_repo = pull_request_obj.target_repo
1030
1030
1031 pr_url = h.route_url('pullrequest_show',
1031 pr_url = h.route_url('pullrequest_show',
1032 repo_name=pr_target_repo.repo_name,
1032 repo_name=pr_target_repo.repo_name,
1033 pull_request_id=pull_request_obj.pull_request_id,)
1033 pull_request_id=pull_request_obj.pull_request_id,)
1034
1034
1035 # set some variables for email notification
1035 # set some variables for email notification
1036 pr_target_repo_url = h.route_url(
1036 pr_target_repo_url = h.route_url(
1037 'repo_summary', repo_name=pr_target_repo.repo_name)
1037 'repo_summary', repo_name=pr_target_repo.repo_name)
1038
1038
1039 pr_source_repo_url = h.route_url(
1039 pr_source_repo_url = h.route_url(
1040 'repo_summary', repo_name=pr_source_repo.repo_name)
1040 'repo_summary', repo_name=pr_source_repo.repo_name)
1041
1041
1042 # pull request specifics
1042 # pull request specifics
1043 pull_request_commits = [
1043 pull_request_commits = [
1044 (x.raw_id, x.message)
1044 (x.raw_id, x.message)
1045 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1045 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1046
1046
1047 kwargs = {
1047 kwargs = {
1048 'user': pull_request.author,
1048 'user': pull_request.author,
1049 'pull_request': pull_request_obj,
1049 'pull_request': pull_request_obj,
1050 'pull_request_commits': pull_request_commits,
1050 'pull_request_commits': pull_request_commits,
1051
1051
1052 'pull_request_target_repo': pr_target_repo,
1052 'pull_request_target_repo': pr_target_repo,
1053 'pull_request_target_repo_url': pr_target_repo_url,
1053 'pull_request_target_repo_url': pr_target_repo_url,
1054
1054
1055 'pull_request_source_repo': pr_source_repo,
1055 'pull_request_source_repo': pr_source_repo,
1056 'pull_request_source_repo_url': pr_source_repo_url,
1056 'pull_request_source_repo_url': pr_source_repo_url,
1057
1057
1058 'pull_request_url': pr_url,
1058 'pull_request_url': pr_url,
1059 }
1059 }
1060
1060
1061 # pre-generate the subject for notification itself
1061 # pre-generate the subject for notification itself
1062 (subject,
1062 (subject,
1063 _h, _e, # we don't care about those
1063 _h, _e, # we don't care about those
1064 body_plaintext) = EmailNotificationModel().render_email(
1064 body_plaintext) = EmailNotificationModel().render_email(
1065 notification_type, **kwargs)
1065 notification_type, **kwargs)
1066
1066
1067 # create notification objects, and emails
1067 # create notification objects, and emails
1068 NotificationModel().create(
1068 NotificationModel().create(
1069 created_by=pull_request.author,
1069 created_by=pull_request.author,
1070 notification_subject=subject,
1070 notification_subject=subject,
1071 notification_body=body_plaintext,
1071 notification_body=body_plaintext,
1072 notification_type=notification_type,
1072 notification_type=notification_type,
1073 recipients=recipients,
1073 recipients=recipients,
1074 email_kwargs=kwargs,
1074 email_kwargs=kwargs,
1075 )
1075 )
1076
1076
1077 def delete(self, pull_request, user):
1077 def delete(self, pull_request, user):
1078 pull_request = self.__get_pull_request(pull_request)
1078 pull_request = self.__get_pull_request(pull_request)
1079 old_data = pull_request.get_api_data(with_merge_state=False)
1079 old_data = pull_request.get_api_data(with_merge_state=False)
1080 self._cleanup_merge_workspace(pull_request)
1080 self._cleanup_merge_workspace(pull_request)
1081 self._log_audit_action(
1081 self._log_audit_action(
1082 'repo.pull_request.delete', {'old_data': old_data},
1082 'repo.pull_request.delete', {'old_data': old_data},
1083 user, pull_request)
1083 user, pull_request)
1084 Session().delete(pull_request)
1084 Session().delete(pull_request)
1085
1085
1086 def close_pull_request(self, pull_request, user):
1086 def close_pull_request(self, pull_request, user):
1087 pull_request = self.__get_pull_request(pull_request)
1087 pull_request = self.__get_pull_request(pull_request)
1088 self._cleanup_merge_workspace(pull_request)
1088 self._cleanup_merge_workspace(pull_request)
1089 pull_request.status = PullRequest.STATUS_CLOSED
1089 pull_request.status = PullRequest.STATUS_CLOSED
1090 pull_request.updated_on = datetime.datetime.now()
1090 pull_request.updated_on = datetime.datetime.now()
1091 Session().add(pull_request)
1091 Session().add(pull_request)
1092 self._trigger_pull_request_hook(
1092 self._trigger_pull_request_hook(
1093 pull_request, pull_request.author, 'close')
1093 pull_request, pull_request.author, 'close')
1094 self._log_audit_action(
1094 self._log_audit_action(
1095 'repo.pull_request.close', {}, user, pull_request)
1095 'repo.pull_request.close', {}, user, pull_request)
1096
1096
1097 def close_pull_request_with_comment(
1097 def close_pull_request_with_comment(
1098 self, pull_request, user, repo, message=None):
1098 self, pull_request, user, repo, message=None):
1099
1099
1100 pull_request_review_status = pull_request.calculated_review_status()
1100 pull_request_review_status = pull_request.calculated_review_status()
1101
1101
1102 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1102 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1103 # approved only if we have voting consent
1103 # approved only if we have voting consent
1104 status = ChangesetStatus.STATUS_APPROVED
1104 status = ChangesetStatus.STATUS_APPROVED
1105 else:
1105 else:
1106 status = ChangesetStatus.STATUS_REJECTED
1106 status = ChangesetStatus.STATUS_REJECTED
1107 status_lbl = ChangesetStatus.get_status_lbl(status)
1107 status_lbl = ChangesetStatus.get_status_lbl(status)
1108
1108
1109 default_message = (
1109 default_message = (
1110 _('Closing with status change {transition_icon} {status}.')
1110 _('Closing with status change {transition_icon} {status}.')
1111 ).format(transition_icon='>', status=status_lbl)
1111 ).format(transition_icon='>', status=status_lbl)
1112 text = message or default_message
1112 text = message or default_message
1113
1113
1114 # create a comment, and link it to new status
1114 # create a comment, and link it to new status
1115 comment = CommentsModel().create(
1115 comment = CommentsModel().create(
1116 text=text,
1116 text=text,
1117 repo=repo.repo_id,
1117 repo=repo.repo_id,
1118 user=user.user_id,
1118 user=user.user_id,
1119 pull_request=pull_request.pull_request_id,
1119 pull_request=pull_request.pull_request_id,
1120 status_change=status_lbl,
1120 status_change=status_lbl,
1121 status_change_type=status,
1121 status_change_type=status,
1122 closing_pr=True
1122 closing_pr=True
1123 )
1123 )
1124
1124
1125 # calculate old status before we change it
1125 # calculate old status before we change it
1126 old_calculated_status = pull_request.calculated_review_status()
1126 old_calculated_status = pull_request.calculated_review_status()
1127 ChangesetStatusModel().set_status(
1127 ChangesetStatusModel().set_status(
1128 repo.repo_id,
1128 repo.repo_id,
1129 status,
1129 status,
1130 user.user_id,
1130 user.user_id,
1131 comment=comment,
1131 comment=comment,
1132 pull_request=pull_request.pull_request_id
1132 pull_request=pull_request.pull_request_id
1133 )
1133 )
1134
1134
1135 Session().flush()
1135 Session().flush()
1136 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1136 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1137 # we now calculate the status of pull request again, and based on that
1137 # we now calculate the status of pull request again, and based on that
1138 # calculation trigger status change. This might happen in cases
1138 # calculation trigger status change. This might happen in cases
1139 # that non-reviewer admin closes a pr, which means his vote doesn't
1139 # that non-reviewer admin closes a pr, which means his vote doesn't
1140 # change the status, while if he's a reviewer this might change it.
1140 # change the status, while if he's a reviewer this might change it.
1141 calculated_status = pull_request.calculated_review_status()
1141 calculated_status = pull_request.calculated_review_status()
1142 if old_calculated_status != calculated_status:
1142 if old_calculated_status != calculated_status:
1143 self._trigger_pull_request_hook(
1143 self._trigger_pull_request_hook(
1144 pull_request, user, 'review_status_change')
1144 pull_request, user, 'review_status_change')
1145
1145
1146 # finally close the PR
1146 # finally close the PR
1147 PullRequestModel().close_pull_request(
1147 PullRequestModel().close_pull_request(
1148 pull_request.pull_request_id, user)
1148 pull_request.pull_request_id, user)
1149
1149
1150 return comment, status
1150 return comment, status
1151
1151
1152 def merge_status(self, pull_request):
1152 def merge_status(self, pull_request):
1153 if not self._is_merge_enabled(pull_request):
1153 if not self._is_merge_enabled(pull_request):
1154 return False, _('Server-side pull request merging is disabled.')
1154 return False, _('Server-side pull request merging is disabled.')
1155 if pull_request.is_closed():
1155 if pull_request.is_closed():
1156 return False, _('This pull request is closed.')
1156 return False, _('This pull request is closed.')
1157 merge_possible, msg = self._check_repo_requirements(
1157 merge_possible, msg = self._check_repo_requirements(
1158 target=pull_request.target_repo, source=pull_request.source_repo)
1158 target=pull_request.target_repo, source=pull_request.source_repo)
1159 if not merge_possible:
1159 if not merge_possible:
1160 return merge_possible, msg
1160 return merge_possible, msg
1161
1161
1162 try:
1162 try:
1163 resp = self._try_merge(pull_request)
1163 resp = self._try_merge(pull_request)
1164 log.debug("Merge response: %s", resp)
1164 log.debug("Merge response: %s", resp)
1165 status = resp.possible, self.merge_status_message(
1165 status = resp.possible, self.merge_status_message(
1166 resp.failure_reason)
1166 resp.failure_reason)
1167 except NotImplementedError:
1167 except NotImplementedError:
1168 status = False, _('Pull request merging is not supported.')
1168 status = False, _('Pull request merging is not supported.')
1169
1169
1170 return status
1170 return status
1171
1171
1172 def _check_repo_requirements(self, target, source):
1172 def _check_repo_requirements(self, target, source):
1173 """
1173 """
1174 Check if `target` and `source` have compatible requirements.
1174 Check if `target` and `source` have compatible requirements.
1175
1175
1176 Currently this is just checking for largefiles.
1176 Currently this is just checking for largefiles.
1177 """
1177 """
1178 target_has_largefiles = self._has_largefiles(target)
1178 target_has_largefiles = self._has_largefiles(target)
1179 source_has_largefiles = self._has_largefiles(source)
1179 source_has_largefiles = self._has_largefiles(source)
1180 merge_possible = True
1180 merge_possible = True
1181 message = u''
1181 message = u''
1182
1182
1183 if target_has_largefiles != source_has_largefiles:
1183 if target_has_largefiles != source_has_largefiles:
1184 merge_possible = False
1184 merge_possible = False
1185 if source_has_largefiles:
1185 if source_has_largefiles:
1186 message = _(
1186 message = _(
1187 'Target repository large files support is disabled.')
1187 'Target repository large files support is disabled.')
1188 else:
1188 else:
1189 message = _(
1189 message = _(
1190 'Source repository large files support is disabled.')
1190 'Source repository large files support is disabled.')
1191
1191
1192 return merge_possible, message
1192 return merge_possible, message
1193
1193
1194 def _has_largefiles(self, repo):
1194 def _has_largefiles(self, repo):
1195 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1195 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1196 'extensions', 'largefiles')
1196 'extensions', 'largefiles')
1197 return largefiles_ui and largefiles_ui[0].active
1197 return largefiles_ui and largefiles_ui[0].active
1198
1198
1199 def _try_merge(self, pull_request):
1199 def _try_merge(self, pull_request):
1200 """
1200 """
1201 Try to merge the pull request and return the merge status.
1201 Try to merge the pull request and return the merge status.
1202 """
1202 """
1203 log.debug(
1203 log.debug(
1204 "Trying out if the pull request %s can be merged.",
1204 "Trying out if the pull request %s can be merged.",
1205 pull_request.pull_request_id)
1205 pull_request.pull_request_id)
1206 target_vcs = pull_request.target_repo.scm_instance()
1206 target_vcs = pull_request.target_repo.scm_instance()
1207
1207
1208 # Refresh the target reference.
1208 # Refresh the target reference.
1209 try:
1209 try:
1210 target_ref = self._refresh_reference(
1210 target_ref = self._refresh_reference(
1211 pull_request.target_ref_parts, target_vcs)
1211 pull_request.target_ref_parts, target_vcs)
1212 except CommitDoesNotExistError:
1212 except CommitDoesNotExistError:
1213 merge_state = MergeResponse(
1213 merge_state = MergeResponse(
1214 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1214 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1215 return merge_state
1215 return merge_state
1216
1216
1217 target_locked = pull_request.target_repo.locked
1217 target_locked = pull_request.target_repo.locked
1218 if target_locked and target_locked[0]:
1218 if target_locked and target_locked[0]:
1219 log.debug("The target repository is locked.")
1219 log.debug("The target repository is locked.")
1220 merge_state = MergeResponse(
1220 merge_state = MergeResponse(
1221 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1221 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1222 elif self._needs_merge_state_refresh(pull_request, target_ref):
1222 elif self._needs_merge_state_refresh(pull_request, target_ref):
1223 log.debug("Refreshing the merge status of the repository.")
1223 log.debug("Refreshing the merge status of the repository.")
1224 merge_state = self._refresh_merge_state(
1224 merge_state = self._refresh_merge_state(
1225 pull_request, target_vcs, target_ref)
1225 pull_request, target_vcs, target_ref)
1226 else:
1226 else:
1227 possible = pull_request.\
1227 possible = pull_request.\
1228 last_merge_status == MergeFailureReason.NONE
1228 last_merge_status == MergeFailureReason.NONE
1229 merge_state = MergeResponse(
1229 merge_state = MergeResponse(
1230 possible, False, None, pull_request.last_merge_status)
1230 possible, False, None, pull_request.last_merge_status)
1231
1231
1232 return merge_state
1232 return merge_state
1233
1233
1234 def _refresh_reference(self, reference, vcs_repository):
1234 def _refresh_reference(self, reference, vcs_repository):
1235 if reference.type in ('branch', 'book'):
1235 if reference.type in ('branch', 'book'):
1236 name_or_id = reference.name
1236 name_or_id = reference.name
1237 else:
1237 else:
1238 name_or_id = reference.commit_id
1238 name_or_id = reference.commit_id
1239 refreshed_commit = vcs_repository.get_commit(name_or_id)
1239 refreshed_commit = vcs_repository.get_commit(name_or_id)
1240 refreshed_reference = Reference(
1240 refreshed_reference = Reference(
1241 reference.type, reference.name, refreshed_commit.raw_id)
1241 reference.type, reference.name, refreshed_commit.raw_id)
1242 return refreshed_reference
1242 return refreshed_reference
1243
1243
1244 def _needs_merge_state_refresh(self, pull_request, target_reference):
1244 def _needs_merge_state_refresh(self, pull_request, target_reference):
1245 return not(
1245 return not(
1246 pull_request.revisions and
1246 pull_request.revisions and
1247 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1247 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1248 target_reference.commit_id == pull_request._last_merge_target_rev)
1248 target_reference.commit_id == pull_request._last_merge_target_rev)
1249
1249
1250 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1250 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1251 workspace_id = self._workspace_id(pull_request)
1251 workspace_id = self._workspace_id(pull_request)
1252 source_vcs = pull_request.source_repo.scm_instance()
1252 source_vcs = pull_request.source_repo.scm_instance()
1253 use_rebase = self._use_rebase_for_merging(pull_request)
1253 use_rebase = self._use_rebase_for_merging(pull_request)
1254 close_branch = self._close_branch_before_merging(pull_request)
1254 close_branch = self._close_branch_before_merging(pull_request)
1255 merge_state = target_vcs.merge(
1255 merge_state = target_vcs.merge(
1256 target_reference, source_vcs, pull_request.source_ref_parts,
1256 target_reference, source_vcs, pull_request.source_ref_parts,
1257 workspace_id, dry_run=True, use_rebase=use_rebase,
1257 workspace_id, dry_run=True, use_rebase=use_rebase,
1258 close_branch=close_branch)
1258 close_branch=close_branch)
1259
1259
1260 # Do not store the response if there was an unknown error.
1260 # Do not store the response if there was an unknown error.
1261 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1261 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1262 pull_request._last_merge_source_rev = \
1262 pull_request._last_merge_source_rev = \
1263 pull_request.source_ref_parts.commit_id
1263 pull_request.source_ref_parts.commit_id
1264 pull_request._last_merge_target_rev = target_reference.commit_id
1264 pull_request._last_merge_target_rev = target_reference.commit_id
1265 pull_request.last_merge_status = merge_state.failure_reason
1265 pull_request.last_merge_status = merge_state.failure_reason
1266 pull_request.shadow_merge_ref = merge_state.merge_ref
1266 pull_request.shadow_merge_ref = merge_state.merge_ref
1267 Session().add(pull_request)
1267 Session().add(pull_request)
1268 Session().commit()
1268 Session().commit()
1269
1269
1270 return merge_state
1270 return merge_state
1271
1271
1272 def _workspace_id(self, pull_request):
1272 def _workspace_id(self, pull_request):
1273 workspace_id = 'pr-%s' % pull_request.pull_request_id
1273 workspace_id = 'pr-%s' % pull_request.pull_request_id
1274 return workspace_id
1274 return workspace_id
1275
1275
1276 def merge_status_message(self, status_code):
1276 def merge_status_message(self, status_code):
1277 """
1277 """
1278 Return a human friendly error message for the given merge status code.
1278 Return a human friendly error message for the given merge status code.
1279 """
1279 """
1280 return self.MERGE_STATUS_MESSAGES[status_code]
1280 return self.MERGE_STATUS_MESSAGES[status_code]
1281
1281
1282 def generate_repo_data(self, repo, commit_id=None, branch=None,
1282 def generate_repo_data(self, repo, commit_id=None, branch=None,
1283 bookmark=None):
1283 bookmark=None):
1284 all_refs, selected_ref = \
1284 all_refs, selected_ref = \
1285 self._get_repo_pullrequest_sources(
1285 self._get_repo_pullrequest_sources(
1286 repo.scm_instance(), commit_id=commit_id,
1286 repo.scm_instance(), commit_id=commit_id,
1287 branch=branch, bookmark=bookmark)
1287 branch=branch, bookmark=bookmark)
1288
1288
1289 refs_select2 = []
1289 refs_select2 = []
1290 for element in all_refs:
1290 for element in all_refs:
1291 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1291 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1292 refs_select2.append({'text': element[1], 'children': children})
1292 refs_select2.append({'text': element[1], 'children': children})
1293
1293
1294 return {
1294 return {
1295 'user': {
1295 'user': {
1296 'user_id': repo.user.user_id,
1296 'user_id': repo.user.user_id,
1297 'username': repo.user.username,
1297 'username': repo.user.username,
1298 'firstname': repo.user.first_name,
1298 'firstname': repo.user.first_name,
1299 'lastname': repo.user.last_name,
1299 'lastname': repo.user.last_name,
1300 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1300 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1301 },
1301 },
1302 'description': h.chop_at_smart(repo.description_safe, '\n'),
1302 'description': h.chop_at_smart(repo.description_safe, '\n'),
1303 'refs': {
1303 'refs': {
1304 'all_refs': all_refs,
1304 'all_refs': all_refs,
1305 'selected_ref': selected_ref,
1305 'selected_ref': selected_ref,
1306 'select2_refs': refs_select2
1306 'select2_refs': refs_select2
1307 }
1307 }
1308 }
1308 }
1309
1309
1310 def generate_pullrequest_title(self, source, source_ref, target):
1310 def generate_pullrequest_title(self, source, source_ref, target):
1311 return u'{source}#{at_ref} to {target}'.format(
1311 return u'{source}#{at_ref} to {target}'.format(
1312 source=source,
1312 source=source,
1313 at_ref=source_ref,
1313 at_ref=source_ref,
1314 target=target,
1314 target=target,
1315 )
1315 )
1316
1316
1317 def _cleanup_merge_workspace(self, pull_request):
1317 def _cleanup_merge_workspace(self, pull_request):
1318 # Merging related cleanup
1318 # Merging related cleanup
1319 target_scm = pull_request.target_repo.scm_instance()
1319 target_scm = pull_request.target_repo.scm_instance()
1320 workspace_id = 'pr-%s' % pull_request.pull_request_id
1320 workspace_id = 'pr-%s' % pull_request.pull_request_id
1321
1321
1322 try:
1322 try:
1323 target_scm.cleanup_merge_workspace(workspace_id)
1323 target_scm.cleanup_merge_workspace(workspace_id)
1324 except NotImplementedError:
1324 except NotImplementedError:
1325 pass
1325 pass
1326
1326
1327 def _get_repo_pullrequest_sources(
1327 def _get_repo_pullrequest_sources(
1328 self, repo, commit_id=None, branch=None, bookmark=None):
1328 self, repo, commit_id=None, branch=None, bookmark=None):
1329 """
1329 """
1330 Return a structure with repo's interesting commits, suitable for
1330 Return a structure with repo's interesting commits, suitable for
1331 the selectors in pullrequest controller
1331 the selectors in pullrequest controller
1332
1332
1333 :param commit_id: a commit that must be in the list somehow
1333 :param commit_id: a commit that must be in the list somehow
1334 and selected by default
1334 and selected by default
1335 :param branch: a branch that must be in the list and selected
1335 :param branch: a branch that must be in the list and selected
1336 by default - even if closed
1336 by default - even if closed
1337 :param bookmark: a bookmark that must be in the list and selected
1337 :param bookmark: a bookmark that must be in the list and selected
1338 """
1338 """
1339
1339
1340 commit_id = safe_str(commit_id) if commit_id else None
1340 commit_id = safe_str(commit_id) if commit_id else None
1341 branch = safe_str(branch) if branch else None
1341 branch = safe_str(branch) if branch else None
1342 bookmark = safe_str(bookmark) if bookmark else None
1342 bookmark = safe_str(bookmark) if bookmark else None
1343
1343
1344 selected = None
1344 selected = None
1345
1345
1346 # order matters: first source that has commit_id in it will be selected
1346 # order matters: first source that has commit_id in it will be selected
1347 sources = []
1347 sources = []
1348 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1348 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1349 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1349 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1350
1350
1351 if commit_id:
1351 if commit_id:
1352 ref_commit = (h.short_id(commit_id), commit_id)
1352 ref_commit = (h.short_id(commit_id), commit_id)
1353 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1353 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1354
1354
1355 sources.append(
1355 sources.append(
1356 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1356 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1357 )
1357 )
1358
1358
1359 groups = []
1359 groups = []
1360 for group_key, ref_list, group_name, match in sources:
1360 for group_key, ref_list, group_name, match in sources:
1361 group_refs = []
1361 group_refs = []
1362 for ref_name, ref_id in ref_list:
1362 for ref_name, ref_id in ref_list:
1363 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1363 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1364 group_refs.append((ref_key, ref_name))
1364 group_refs.append((ref_key, ref_name))
1365
1365
1366 if not selected:
1366 if not selected:
1367 if set([commit_id, match]) & set([ref_id, ref_name]):
1367 if set([commit_id, match]) & set([ref_id, ref_name]):
1368 selected = ref_key
1368 selected = ref_key
1369
1369
1370 if group_refs:
1370 if group_refs:
1371 groups.append((group_refs, group_name))
1371 groups.append((group_refs, group_name))
1372
1372
1373 if not selected:
1373 if not selected:
1374 ref = commit_id or branch or bookmark
1374 ref = commit_id or branch or bookmark
1375 if ref:
1375 if ref:
1376 raise CommitDoesNotExistError(
1376 raise CommitDoesNotExistError(
1377 'No commit refs could be found matching: %s' % ref)
1377 'No commit refs could be found matching: %s' % ref)
1378 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1378 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1379 selected = 'branch:%s:%s' % (
1379 selected = 'branch:%s:%s' % (
1380 repo.DEFAULT_BRANCH_NAME,
1380 repo.DEFAULT_BRANCH_NAME,
1381 repo.branches[repo.DEFAULT_BRANCH_NAME]
1381 repo.branches[repo.DEFAULT_BRANCH_NAME]
1382 )
1382 )
1383 elif repo.commit_ids:
1383 elif repo.commit_ids:
1384 rev = repo.commit_ids[0]
1384 rev = repo.commit_ids[0]
1385 selected = 'rev:%s:%s' % (rev, rev)
1385 selected = 'rev:%s:%s' % (rev, rev)
1386 else:
1386 else:
1387 raise EmptyRepositoryError()
1387 raise EmptyRepositoryError()
1388 return groups, selected
1388 return groups, selected
1389
1389
1390 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1390 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1391 return self._get_diff_from_pr_or_version(
1391 return self._get_diff_from_pr_or_version(
1392 source_repo, source_ref_id, target_ref_id, context=context)
1392 source_repo, source_ref_id, target_ref_id, context=context)
1393
1393
1394 def _get_diff_from_pr_or_version(
1394 def _get_diff_from_pr_or_version(
1395 self, source_repo, source_ref_id, target_ref_id, context):
1395 self, source_repo, source_ref_id, target_ref_id, context):
1396 target_commit = source_repo.get_commit(
1396 target_commit = source_repo.get_commit(
1397 commit_id=safe_str(target_ref_id))
1397 commit_id=safe_str(target_ref_id))
1398 source_commit = source_repo.get_commit(
1398 source_commit = source_repo.get_commit(
1399 commit_id=safe_str(source_ref_id))
1399 commit_id=safe_str(source_ref_id))
1400 if isinstance(source_repo, Repository):
1400 if isinstance(source_repo, Repository):
1401 vcs_repo = source_repo.scm_instance()
1401 vcs_repo = source_repo.scm_instance()
1402 else:
1402 else:
1403 vcs_repo = source_repo
1403 vcs_repo = source_repo
1404
1404
1405 # TODO: johbo: In the context of an update, we cannot reach
1405 # TODO: johbo: In the context of an update, we cannot reach
1406 # the old commit anymore with our normal mechanisms. It needs
1406 # the old commit anymore with our normal mechanisms. It needs
1407 # some sort of special support in the vcs layer to avoid this
1407 # some sort of special support in the vcs layer to avoid this
1408 # workaround.
1408 # workaround.
1409 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1409 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1410 vcs_repo.alias == 'git'):
1410 vcs_repo.alias == 'git'):
1411 source_commit.raw_id = safe_str(source_ref_id)
1411 source_commit.raw_id = safe_str(source_ref_id)
1412
1412
1413 log.debug('calculating diff between '
1413 log.debug('calculating diff between '
1414 'source_ref:%s and target_ref:%s for repo `%s`',
1414 'source_ref:%s and target_ref:%s for repo `%s`',
1415 target_ref_id, source_ref_id,
1415 target_ref_id, source_ref_id,
1416 safe_unicode(vcs_repo.path))
1416 safe_unicode(vcs_repo.path))
1417
1417
1418 vcs_diff = vcs_repo.get_diff(
1418 vcs_diff = vcs_repo.get_diff(
1419 commit1=target_commit, commit2=source_commit, context=context)
1419 commit1=target_commit, commit2=source_commit, context=context)
1420 return vcs_diff
1420 return vcs_diff
1421
1421
1422 def _is_merge_enabled(self, pull_request):
1422 def _is_merge_enabled(self, pull_request):
1423 return self._get_general_setting(
1423 return self._get_general_setting(
1424 pull_request, 'rhodecode_pr_merge_enabled')
1424 pull_request, 'rhodecode_pr_merge_enabled')
1425
1425
1426 def _use_rebase_for_merging(self, pull_request):
1426 def _use_rebase_for_merging(self, pull_request):
1427 repo_type = pull_request.target_repo.repo_type
1428 if repo_type == 'hg':
1427 return self._get_general_setting(
1429 return self._get_general_setting(
1428 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1430 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1431 elif repo_type == 'git':
1432 return self._get_general_setting(
1433 pull_request, 'rhodecode_git_use_rebase_for_merging')
1434
1435 return False
1429
1436
1430 def _close_branch_before_merging(self, pull_request):
1437 def _close_branch_before_merging(self, pull_request):
1438 repo_type = pull_request.target_repo.repo_type
1439 if repo_type == 'hg':
1431 return self._get_general_setting(
1440 return self._get_general_setting(
1432 pull_request, 'rhodecode_hg_close_branch_before_merging')
1441 pull_request, 'rhodecode_hg_close_branch_before_merging')
1442 elif repo_type == 'git':
1443 return self._get_general_setting(
1444 pull_request, 'rhodecode_git_close_branch_before_merging')
1445
1446 return False
1433
1447
1434 def _get_general_setting(self, pull_request, settings_key, default=False):
1448 def _get_general_setting(self, pull_request, settings_key, default=False):
1435 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1449 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1436 settings = settings_model.get_general_settings()
1450 settings = settings_model.get_general_settings()
1437 return settings.get(settings_key, default)
1451 return settings.get(settings_key, default)
1438
1452
1439 def _log_audit_action(self, action, action_data, user, pull_request):
1453 def _log_audit_action(self, action, action_data, user, pull_request):
1440 audit_logger.store(
1454 audit_logger.store(
1441 action=action,
1455 action=action,
1442 action_data=action_data,
1456 action_data=action_data,
1443 user=user,
1457 user=user,
1444 repo=pull_request.target_repo)
1458 repo=pull_request.target_repo)
1445
1459
1446 def get_reviewer_functions(self):
1460 def get_reviewer_functions(self):
1447 """
1461 """
1448 Fetches functions for validation and fetching default reviewers.
1462 Fetches functions for validation and fetching default reviewers.
1449 If available we use the EE package, else we fallback to CE
1463 If available we use the EE package, else we fallback to CE
1450 package functions
1464 package functions
1451 """
1465 """
1452 try:
1466 try:
1453 from rc_reviewers.utils import get_default_reviewers_data
1467 from rc_reviewers.utils import get_default_reviewers_data
1454 from rc_reviewers.utils import validate_default_reviewers
1468 from rc_reviewers.utils import validate_default_reviewers
1455 except ImportError:
1469 except ImportError:
1456 from rhodecode.apps.repository.utils import \
1470 from rhodecode.apps.repository.utils import \
1457 get_default_reviewers_data
1471 get_default_reviewers_data
1458 from rhodecode.apps.repository.utils import \
1472 from rhodecode.apps.repository.utils import \
1459 validate_default_reviewers
1473 validate_default_reviewers
1460
1474
1461 return get_default_reviewers_data, validate_default_reviewers
1475 return get_default_reviewers_data, validate_default_reviewers
1462
1476
1463
1477
1464 class MergeCheck(object):
1478 class MergeCheck(object):
1465 """
1479 """
1466 Perform Merge Checks and returns a check object which stores information
1480 Perform Merge Checks and returns a check object which stores information
1467 about merge errors, and merge conditions
1481 about merge errors, and merge conditions
1468 """
1482 """
1469 TODO_CHECK = 'todo'
1483 TODO_CHECK = 'todo'
1470 PERM_CHECK = 'perm'
1484 PERM_CHECK = 'perm'
1471 REVIEW_CHECK = 'review'
1485 REVIEW_CHECK = 'review'
1472 MERGE_CHECK = 'merge'
1486 MERGE_CHECK = 'merge'
1473
1487
1474 def __init__(self):
1488 def __init__(self):
1475 self.review_status = None
1489 self.review_status = None
1476 self.merge_possible = None
1490 self.merge_possible = None
1477 self.merge_msg = ''
1491 self.merge_msg = ''
1478 self.failed = None
1492 self.failed = None
1479 self.errors = []
1493 self.errors = []
1480 self.error_details = OrderedDict()
1494 self.error_details = OrderedDict()
1481
1495
1482 def push_error(self, error_type, message, error_key, details):
1496 def push_error(self, error_type, message, error_key, details):
1483 self.failed = True
1497 self.failed = True
1484 self.errors.append([error_type, message])
1498 self.errors.append([error_type, message])
1485 self.error_details[error_key] = dict(
1499 self.error_details[error_key] = dict(
1486 details=details,
1500 details=details,
1487 error_type=error_type,
1501 error_type=error_type,
1488 message=message
1502 message=message
1489 )
1503 )
1490
1504
1491 @classmethod
1505 @classmethod
1492 def validate(cls, pull_request, user, fail_early=False, translator=None):
1506 def validate(cls, pull_request, user, fail_early=False, translator=None):
1493 # if migrated to pyramid...
1507 # if migrated to pyramid...
1494 # _ = lambda: translator or _ # use passed in translator if any
1508 # _ = lambda: translator or _ # use passed in translator if any
1495
1509
1496 merge_check = cls()
1510 merge_check = cls()
1497
1511
1498 # permissions to merge
1512 # permissions to merge
1499 user_allowed_to_merge = PullRequestModel().check_user_merge(
1513 user_allowed_to_merge = PullRequestModel().check_user_merge(
1500 pull_request, user)
1514 pull_request, user)
1501 if not user_allowed_to_merge:
1515 if not user_allowed_to_merge:
1502 log.debug("MergeCheck: cannot merge, approval is pending.")
1516 log.debug("MergeCheck: cannot merge, approval is pending.")
1503
1517
1504 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1518 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1505 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1519 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1506 if fail_early:
1520 if fail_early:
1507 return merge_check
1521 return merge_check
1508
1522
1509 # review status, must be always present
1523 # review status, must be always present
1510 review_status = pull_request.calculated_review_status()
1524 review_status = pull_request.calculated_review_status()
1511 merge_check.review_status = review_status
1525 merge_check.review_status = review_status
1512
1526
1513 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1527 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1514 if not status_approved:
1528 if not status_approved:
1515 log.debug("MergeCheck: cannot merge, approval is pending.")
1529 log.debug("MergeCheck: cannot merge, approval is pending.")
1516
1530
1517 msg = _('Pull request reviewer approval is pending.')
1531 msg = _('Pull request reviewer approval is pending.')
1518
1532
1519 merge_check.push_error(
1533 merge_check.push_error(
1520 'warning', msg, cls.REVIEW_CHECK, review_status)
1534 'warning', msg, cls.REVIEW_CHECK, review_status)
1521
1535
1522 if fail_early:
1536 if fail_early:
1523 return merge_check
1537 return merge_check
1524
1538
1525 # left over TODOs
1539 # left over TODOs
1526 todos = CommentsModel().get_unresolved_todos(pull_request)
1540 todos = CommentsModel().get_unresolved_todos(pull_request)
1527 if todos:
1541 if todos:
1528 log.debug("MergeCheck: cannot merge, {} "
1542 log.debug("MergeCheck: cannot merge, {} "
1529 "unresolved todos left.".format(len(todos)))
1543 "unresolved todos left.".format(len(todos)))
1530
1544
1531 if len(todos) == 1:
1545 if len(todos) == 1:
1532 msg = _('Cannot merge, {} TODO still not resolved.').format(
1546 msg = _('Cannot merge, {} TODO still not resolved.').format(
1533 len(todos))
1547 len(todos))
1534 else:
1548 else:
1535 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1549 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1536 len(todos))
1550 len(todos))
1537
1551
1538 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1552 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1539
1553
1540 if fail_early:
1554 if fail_early:
1541 return merge_check
1555 return merge_check
1542
1556
1543 # merge possible
1557 # merge possible
1544 merge_status, msg = PullRequestModel().merge_status(pull_request)
1558 merge_status, msg = PullRequestModel().merge_status(pull_request)
1545 merge_check.merge_possible = merge_status
1559 merge_check.merge_possible = merge_status
1546 merge_check.merge_msg = msg
1560 merge_check.merge_msg = msg
1547 if not merge_status:
1561 if not merge_status:
1548 log.debug(
1562 log.debug(
1549 "MergeCheck: cannot merge, pull request merge not possible.")
1563 "MergeCheck: cannot merge, pull request merge not possible.")
1550 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1564 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1551
1565
1552 if fail_early:
1566 if fail_early:
1553 return merge_check
1567 return merge_check
1554
1568
1555 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1569 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1556 return merge_check
1570 return merge_check
1557
1571
1558 @classmethod
1572 @classmethod
1559 def get_merge_conditions(cls, pull_request):
1573 def get_merge_conditions(cls, pull_request):
1560 merge_details = {}
1574 merge_details = {}
1561
1575
1562 model = PullRequestModel()
1576 model = PullRequestModel()
1563 use_rebase = model._use_rebase_for_merging(pull_request)
1577 use_rebase = model._use_rebase_for_merging(pull_request)
1564
1578
1565 if use_rebase:
1579 if use_rebase:
1566 merge_details['merge_strategy'] = dict(
1580 merge_details['merge_strategy'] = dict(
1567 details={},
1581 details={},
1568 message=_('Merge strategy: rebase')
1582 message=_('Merge strategy: rebase')
1569 )
1583 )
1570 else:
1584 else:
1571 merge_details['merge_strategy'] = dict(
1585 merge_details['merge_strategy'] = dict(
1572 details={},
1586 details={},
1573 message=_('Merge strategy: explicit merge commit')
1587 message=_('Merge strategy: explicit merge commit')
1574 )
1588 )
1575
1589
1576 close_branch = model._close_branch_before_merging(pull_request)
1590 close_branch = model._close_branch_before_merging(pull_request)
1577 if close_branch:
1591 if close_branch:
1578 repo_type = pull_request.target_repo.repo_type
1592 repo_type = pull_request.target_repo.repo_type
1579 if repo_type == 'hg':
1593 if repo_type == 'hg':
1580 close_msg = _('Source branch will be closed after merge.')
1594 close_msg = _('Source branch will be closed after merge.')
1581 elif repo_type == 'git':
1595 elif repo_type == 'git':
1582 close_msg = _('Source branch will be deleted after merge.')
1596 close_msg = _('Source branch will be deleted after merge.')
1583
1597
1584 merge_details['close_branch'] = dict(
1598 merge_details['close_branch'] = dict(
1585 details={},
1599 details={},
1586 message=close_msg
1600 message=close_msg
1587 )
1601 )
1588
1602
1589 return merge_details
1603 return merge_details
1590
1604
1591 ChangeTuple = namedtuple('ChangeTuple',
1605 ChangeTuple = namedtuple('ChangeTuple',
1592 ['added', 'common', 'removed', 'total'])
1606 ['added', 'common', 'removed', 'total'])
1593
1607
1594 FileChangeTuple = namedtuple('FileChangeTuple',
1608 FileChangeTuple = namedtuple('FileChangeTuple',
1595 ['added', 'modified', 'removed'])
1609 ['added', 'modified', 'removed'])
@@ -1,812 +1,814 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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 os
21 import os
22 import hashlib
22 import hashlib
23 import logging
23 import logging
24 from collections import namedtuple
24 from collections import namedtuple
25 from functools import wraps
25 from functools import wraps
26
26
27 from rhodecode.lib import caches
27 from rhodecode.lib import caches
28 from rhodecode.lib.utils2 import (
28 from rhodecode.lib.utils2 import (
29 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
29 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
30 from rhodecode.lib.vcs.backends import base
30 from rhodecode.lib.vcs.backends import base
31 from rhodecode.model import BaseModel
31 from rhodecode.model import BaseModel
32 from rhodecode.model.db import (
32 from rhodecode.model.db import (
33 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
33 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 UiSetting = namedtuple(
40 UiSetting = namedtuple(
41 'UiSetting', ['section', 'key', 'value', 'active'])
41 'UiSetting', ['section', 'key', 'value', 'active'])
42
42
43 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
43 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
44
44
45
45
46 class SettingNotFound(Exception):
46 class SettingNotFound(Exception):
47 def __init__(self):
47 def __init__(self):
48 super(SettingNotFound, self).__init__('Setting is not found')
48 super(SettingNotFound, self).__init__('Setting is not found')
49
49
50
50
51 class SettingsModel(BaseModel):
51 class SettingsModel(BaseModel):
52 BUILTIN_HOOKS = (
52 BUILTIN_HOOKS = (
53 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
53 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
54 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
54 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
55 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
55 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
56 RhodeCodeUi.HOOK_PUSH_KEY,)
56 RhodeCodeUi.HOOK_PUSH_KEY,)
57 HOOKS_SECTION = 'hooks'
57 HOOKS_SECTION = 'hooks'
58
58
59 def __init__(self, sa=None, repo=None):
59 def __init__(self, sa=None, repo=None):
60 self.repo = repo
60 self.repo = repo
61 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
61 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
62 self.SettingsDbModel = (
62 self.SettingsDbModel = (
63 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
63 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
64 super(SettingsModel, self).__init__(sa)
64 super(SettingsModel, self).__init__(sa)
65
65
66 def get_ui_by_key(self, key):
66 def get_ui_by_key(self, key):
67 q = self.UiDbModel.query()
67 q = self.UiDbModel.query()
68 q = q.filter(self.UiDbModel.ui_key == key)
68 q = q.filter(self.UiDbModel.ui_key == key)
69 q = self._filter_by_repo(RepoRhodeCodeUi, q)
69 q = self._filter_by_repo(RepoRhodeCodeUi, q)
70 return q.scalar()
70 return q.scalar()
71
71
72 def get_ui_by_section(self, section):
72 def get_ui_by_section(self, section):
73 q = self.UiDbModel.query()
73 q = self.UiDbModel.query()
74 q = q.filter(self.UiDbModel.ui_section == section)
74 q = q.filter(self.UiDbModel.ui_section == section)
75 q = self._filter_by_repo(RepoRhodeCodeUi, q)
75 q = self._filter_by_repo(RepoRhodeCodeUi, q)
76 return q.all()
76 return q.all()
77
77
78 def get_ui_by_section_and_key(self, section, key):
78 def get_ui_by_section_and_key(self, section, key):
79 q = self.UiDbModel.query()
79 q = self.UiDbModel.query()
80 q = q.filter(self.UiDbModel.ui_section == section)
80 q = q.filter(self.UiDbModel.ui_section == section)
81 q = q.filter(self.UiDbModel.ui_key == key)
81 q = q.filter(self.UiDbModel.ui_key == key)
82 q = self._filter_by_repo(RepoRhodeCodeUi, q)
82 q = self._filter_by_repo(RepoRhodeCodeUi, q)
83 return q.scalar()
83 return q.scalar()
84
84
85 def get_ui(self, section=None, key=None):
85 def get_ui(self, section=None, key=None):
86 q = self.UiDbModel.query()
86 q = self.UiDbModel.query()
87 q = self._filter_by_repo(RepoRhodeCodeUi, q)
87 q = self._filter_by_repo(RepoRhodeCodeUi, q)
88
88
89 if section:
89 if section:
90 q = q.filter(self.UiDbModel.ui_section == section)
90 q = q.filter(self.UiDbModel.ui_section == section)
91 if key:
91 if key:
92 q = q.filter(self.UiDbModel.ui_key == key)
92 q = q.filter(self.UiDbModel.ui_key == key)
93
93
94 # TODO: mikhail: add caching
94 # TODO: mikhail: add caching
95 result = [
95 result = [
96 UiSetting(
96 UiSetting(
97 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
97 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
98 value=safe_str(r.ui_value), active=r.ui_active
98 value=safe_str(r.ui_value), active=r.ui_active
99 )
99 )
100 for r in q.all()
100 for r in q.all()
101 ]
101 ]
102 return result
102 return result
103
103
104 def get_builtin_hooks(self):
104 def get_builtin_hooks(self):
105 q = self.UiDbModel.query()
105 q = self.UiDbModel.query()
106 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
106 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
107 return self._get_hooks(q)
107 return self._get_hooks(q)
108
108
109 def get_custom_hooks(self):
109 def get_custom_hooks(self):
110 q = self.UiDbModel.query()
110 q = self.UiDbModel.query()
111 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
111 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
112 return self._get_hooks(q)
112 return self._get_hooks(q)
113
113
114 def create_ui_section_value(self, section, val, key=None, active=True):
114 def create_ui_section_value(self, section, val, key=None, active=True):
115 new_ui = self.UiDbModel()
115 new_ui = self.UiDbModel()
116 new_ui.ui_section = section
116 new_ui.ui_section = section
117 new_ui.ui_value = val
117 new_ui.ui_value = val
118 new_ui.ui_active = active
118 new_ui.ui_active = active
119
119
120 if self.repo:
120 if self.repo:
121 repo = self._get_repo(self.repo)
121 repo = self._get_repo(self.repo)
122 repository_id = repo.repo_id
122 repository_id = repo.repo_id
123 new_ui.repository_id = repository_id
123 new_ui.repository_id = repository_id
124
124
125 if not key:
125 if not key:
126 # keys are unique so they need appended info
126 # keys are unique so they need appended info
127 if self.repo:
127 if self.repo:
128 key = hashlib.sha1(
128 key = hashlib.sha1(
129 '{}{}{}'.format(section, val, repository_id)).hexdigest()
129 '{}{}{}'.format(section, val, repository_id)).hexdigest()
130 else:
130 else:
131 key = hashlib.sha1('{}{}'.format(section, val)).hexdigest()
131 key = hashlib.sha1('{}{}'.format(section, val)).hexdigest()
132
132
133 new_ui.ui_key = key
133 new_ui.ui_key = key
134
134
135 Session().add(new_ui)
135 Session().add(new_ui)
136 return new_ui
136 return new_ui
137
137
138 def create_or_update_hook(self, key, value):
138 def create_or_update_hook(self, key, value):
139 ui = (
139 ui = (
140 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
140 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
141 self.UiDbModel())
141 self.UiDbModel())
142 ui.ui_section = self.HOOKS_SECTION
142 ui.ui_section = self.HOOKS_SECTION
143 ui.ui_active = True
143 ui.ui_active = True
144 ui.ui_key = key
144 ui.ui_key = key
145 ui.ui_value = value
145 ui.ui_value = value
146
146
147 if self.repo:
147 if self.repo:
148 repo = self._get_repo(self.repo)
148 repo = self._get_repo(self.repo)
149 repository_id = repo.repo_id
149 repository_id = repo.repo_id
150 ui.repository_id = repository_id
150 ui.repository_id = repository_id
151
151
152 Session().add(ui)
152 Session().add(ui)
153 return ui
153 return ui
154
154
155 def delete_ui(self, id_):
155 def delete_ui(self, id_):
156 ui = self.UiDbModel.get(id_)
156 ui = self.UiDbModel.get(id_)
157 if not ui:
157 if not ui:
158 raise SettingNotFound()
158 raise SettingNotFound()
159 Session().delete(ui)
159 Session().delete(ui)
160
160
161 def get_setting_by_name(self, name):
161 def get_setting_by_name(self, name):
162 q = self._get_settings_query()
162 q = self._get_settings_query()
163 q = q.filter(self.SettingsDbModel.app_settings_name == name)
163 q = q.filter(self.SettingsDbModel.app_settings_name == name)
164 return q.scalar()
164 return q.scalar()
165
165
166 def create_or_update_setting(
166 def create_or_update_setting(
167 self, name, val=Optional(''), type_=Optional('unicode')):
167 self, name, val=Optional(''), type_=Optional('unicode')):
168 """
168 """
169 Creates or updates RhodeCode setting. If updates is triggered it will
169 Creates or updates RhodeCode setting. If updates is triggered it will
170 only update parameters that are explicityl set Optional instance will
170 only update parameters that are explicityl set Optional instance will
171 be skipped
171 be skipped
172
172
173 :param name:
173 :param name:
174 :param val:
174 :param val:
175 :param type_:
175 :param type_:
176 :return:
176 :return:
177 """
177 """
178
178
179 res = self.get_setting_by_name(name)
179 res = self.get_setting_by_name(name)
180 repo = self._get_repo(self.repo) if self.repo else None
180 repo = self._get_repo(self.repo) if self.repo else None
181
181
182 if not res:
182 if not res:
183 val = Optional.extract(val)
183 val = Optional.extract(val)
184 type_ = Optional.extract(type_)
184 type_ = Optional.extract(type_)
185
185
186 args = (
186 args = (
187 (repo.repo_id, name, val, type_)
187 (repo.repo_id, name, val, type_)
188 if repo else (name, val, type_))
188 if repo else (name, val, type_))
189 res = self.SettingsDbModel(*args)
189 res = self.SettingsDbModel(*args)
190
190
191 else:
191 else:
192 if self.repo:
192 if self.repo:
193 res.repository_id = repo.repo_id
193 res.repository_id = repo.repo_id
194
194
195 res.app_settings_name = name
195 res.app_settings_name = name
196 if not isinstance(type_, Optional):
196 if not isinstance(type_, Optional):
197 # update if set
197 # update if set
198 res.app_settings_type = type_
198 res.app_settings_type = type_
199 if not isinstance(val, Optional):
199 if not isinstance(val, Optional):
200 # update if set
200 # update if set
201 res.app_settings_value = val
201 res.app_settings_value = val
202
202
203 Session().add(res)
203 Session().add(res)
204 return res
204 return res
205
205
206 def invalidate_settings_cache(self):
206 def invalidate_settings_cache(self):
207 namespace = 'rhodecode_settings'
207 namespace = 'rhodecode_settings'
208 cache_manager = caches.get_cache_manager('sql_cache_short', namespace)
208 cache_manager = caches.get_cache_manager('sql_cache_short', namespace)
209 caches.clear_cache_manager(cache_manager)
209 caches.clear_cache_manager(cache_manager)
210
210
211 def get_all_settings(self, cache=False):
211 def get_all_settings(self, cache=False):
212
212
213 def _compute():
213 def _compute():
214 q = self._get_settings_query()
214 q = self._get_settings_query()
215 if not q:
215 if not q:
216 raise Exception('Could not get application settings !')
216 raise Exception('Could not get application settings !')
217
217
218 settings = {
218 settings = {
219 'rhodecode_' + result.app_settings_name: result.app_settings_value
219 'rhodecode_' + result.app_settings_name: result.app_settings_value
220 for result in q
220 for result in q
221 }
221 }
222 return settings
222 return settings
223
223
224 if cache:
224 if cache:
225 log.debug('Fetching app settings using cache')
225 log.debug('Fetching app settings using cache')
226 repo = self._get_repo(self.repo) if self.repo else None
226 repo = self._get_repo(self.repo) if self.repo else None
227 namespace = 'rhodecode_settings'
227 namespace = 'rhodecode_settings'
228 cache_manager = caches.get_cache_manager(
228 cache_manager = caches.get_cache_manager(
229 'sql_cache_short', namespace)
229 'sql_cache_short', namespace)
230 _cache_key = (
230 _cache_key = (
231 "get_repo_{}_settings".format(repo.repo_id)
231 "get_repo_{}_settings".format(repo.repo_id)
232 if repo else "get_app_settings")
232 if repo else "get_app_settings")
233
233
234 return cache_manager.get(_cache_key, createfunc=_compute)
234 return cache_manager.get(_cache_key, createfunc=_compute)
235
235
236 else:
236 else:
237 return _compute()
237 return _compute()
238
238
239 def get_auth_settings(self):
239 def get_auth_settings(self):
240 q = self._get_settings_query()
240 q = self._get_settings_query()
241 q = q.filter(
241 q = q.filter(
242 self.SettingsDbModel.app_settings_name.startswith('auth_'))
242 self.SettingsDbModel.app_settings_name.startswith('auth_'))
243 rows = q.all()
243 rows = q.all()
244 auth_settings = {
244 auth_settings = {
245 row.app_settings_name: row.app_settings_value for row in rows}
245 row.app_settings_name: row.app_settings_value for row in rows}
246 return auth_settings
246 return auth_settings
247
247
248 def get_auth_plugins(self):
248 def get_auth_plugins(self):
249 auth_plugins = self.get_setting_by_name("auth_plugins")
249 auth_plugins = self.get_setting_by_name("auth_plugins")
250 return auth_plugins.app_settings_value
250 return auth_plugins.app_settings_value
251
251
252 def get_default_repo_settings(self, strip_prefix=False):
252 def get_default_repo_settings(self, strip_prefix=False):
253 q = self._get_settings_query()
253 q = self._get_settings_query()
254 q = q.filter(
254 q = q.filter(
255 self.SettingsDbModel.app_settings_name.startswith('default_'))
255 self.SettingsDbModel.app_settings_name.startswith('default_'))
256 rows = q.all()
256 rows = q.all()
257
257
258 result = {}
258 result = {}
259 for row in rows:
259 for row in rows:
260 key = row.app_settings_name
260 key = row.app_settings_name
261 if strip_prefix:
261 if strip_prefix:
262 key = remove_prefix(key, prefix='default_')
262 key = remove_prefix(key, prefix='default_')
263 result.update({key: row.app_settings_value})
263 result.update({key: row.app_settings_value})
264 return result
264 return result
265
265
266 def get_repo(self):
266 def get_repo(self):
267 repo = self._get_repo(self.repo)
267 repo = self._get_repo(self.repo)
268 if not repo:
268 if not repo:
269 raise Exception(
269 raise Exception(
270 'Repository `{}` cannot be found inside the database'.format(
270 'Repository `{}` cannot be found inside the database'.format(
271 self.repo))
271 self.repo))
272 return repo
272 return repo
273
273
274 def _filter_by_repo(self, model, query):
274 def _filter_by_repo(self, model, query):
275 if self.repo:
275 if self.repo:
276 repo = self.get_repo()
276 repo = self.get_repo()
277 query = query.filter(model.repository_id == repo.repo_id)
277 query = query.filter(model.repository_id == repo.repo_id)
278 return query
278 return query
279
279
280 def _get_hooks(self, query):
280 def _get_hooks(self, query):
281 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
281 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
282 query = self._filter_by_repo(RepoRhodeCodeUi, query)
282 query = self._filter_by_repo(RepoRhodeCodeUi, query)
283 return query.all()
283 return query.all()
284
284
285 def _get_settings_query(self):
285 def _get_settings_query(self):
286 q = self.SettingsDbModel.query()
286 q = self.SettingsDbModel.query()
287 return self._filter_by_repo(RepoRhodeCodeSetting, q)
287 return self._filter_by_repo(RepoRhodeCodeSetting, q)
288
288
289 def list_enabled_social_plugins(self, settings):
289 def list_enabled_social_plugins(self, settings):
290 enabled = []
290 enabled = []
291 for plug in SOCIAL_PLUGINS_LIST:
291 for plug in SOCIAL_PLUGINS_LIST:
292 if str2bool(settings.get('rhodecode_auth_{}_enabled'.format(plug)
292 if str2bool(settings.get('rhodecode_auth_{}_enabled'.format(plug)
293 )):
293 )):
294 enabled.append(plug)
294 enabled.append(plug)
295 return enabled
295 return enabled
296
296
297
297
298 def assert_repo_settings(func):
298 def assert_repo_settings(func):
299 @wraps(func)
299 @wraps(func)
300 def _wrapper(self, *args, **kwargs):
300 def _wrapper(self, *args, **kwargs):
301 if not self.repo_settings:
301 if not self.repo_settings:
302 raise Exception('Repository is not specified')
302 raise Exception('Repository is not specified')
303 return func(self, *args, **kwargs)
303 return func(self, *args, **kwargs)
304 return _wrapper
304 return _wrapper
305
305
306
306
307 class IssueTrackerSettingsModel(object):
307 class IssueTrackerSettingsModel(object):
308 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
308 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
309 SETTINGS_PREFIX = 'issuetracker_'
309 SETTINGS_PREFIX = 'issuetracker_'
310
310
311 def __init__(self, sa=None, repo=None):
311 def __init__(self, sa=None, repo=None):
312 self.global_settings = SettingsModel(sa=sa)
312 self.global_settings = SettingsModel(sa=sa)
313 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
313 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
314
314
315 @property
315 @property
316 def inherit_global_settings(self):
316 def inherit_global_settings(self):
317 if not self.repo_settings:
317 if not self.repo_settings:
318 return True
318 return True
319 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
319 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
320 return setting.app_settings_value if setting else True
320 return setting.app_settings_value if setting else True
321
321
322 @inherit_global_settings.setter
322 @inherit_global_settings.setter
323 def inherit_global_settings(self, value):
323 def inherit_global_settings(self, value):
324 if self.repo_settings:
324 if self.repo_settings:
325 settings = self.repo_settings.create_or_update_setting(
325 settings = self.repo_settings.create_or_update_setting(
326 self.INHERIT_SETTINGS, value, type_='bool')
326 self.INHERIT_SETTINGS, value, type_='bool')
327 Session().add(settings)
327 Session().add(settings)
328
328
329 def _get_keyname(self, key, uid, prefix=''):
329 def _get_keyname(self, key, uid, prefix=''):
330 return '{0}{1}{2}_{3}'.format(
330 return '{0}{1}{2}_{3}'.format(
331 prefix, self.SETTINGS_PREFIX, key, uid)
331 prefix, self.SETTINGS_PREFIX, key, uid)
332
332
333 def _make_dict_for_settings(self, qs):
333 def _make_dict_for_settings(self, qs):
334 prefix_match = self._get_keyname('pat', '', 'rhodecode_')
334 prefix_match = self._get_keyname('pat', '', 'rhodecode_')
335
335
336 issuetracker_entries = {}
336 issuetracker_entries = {}
337 # create keys
337 # create keys
338 for k, v in qs.items():
338 for k, v in qs.items():
339 if k.startswith(prefix_match):
339 if k.startswith(prefix_match):
340 uid = k[len(prefix_match):]
340 uid = k[len(prefix_match):]
341 issuetracker_entries[uid] = None
341 issuetracker_entries[uid] = None
342
342
343 # populate
343 # populate
344 for uid in issuetracker_entries:
344 for uid in issuetracker_entries:
345 issuetracker_entries[uid] = AttributeDict({
345 issuetracker_entries[uid] = AttributeDict({
346 'pat': qs.get(self._get_keyname('pat', uid, 'rhodecode_')),
346 'pat': qs.get(self._get_keyname('pat', uid, 'rhodecode_')),
347 'url': qs.get(self._get_keyname('url', uid, 'rhodecode_')),
347 'url': qs.get(self._get_keyname('url', uid, 'rhodecode_')),
348 'pref': qs.get(self._get_keyname('pref', uid, 'rhodecode_')),
348 'pref': qs.get(self._get_keyname('pref', uid, 'rhodecode_')),
349 'desc': qs.get(self._get_keyname('desc', uid, 'rhodecode_')),
349 'desc': qs.get(self._get_keyname('desc', uid, 'rhodecode_')),
350 })
350 })
351 return issuetracker_entries
351 return issuetracker_entries
352
352
353 def get_global_settings(self, cache=False):
353 def get_global_settings(self, cache=False):
354 """
354 """
355 Returns list of global issue tracker settings
355 Returns list of global issue tracker settings
356 """
356 """
357 defaults = self.global_settings.get_all_settings(cache=cache)
357 defaults = self.global_settings.get_all_settings(cache=cache)
358 settings = self._make_dict_for_settings(defaults)
358 settings = self._make_dict_for_settings(defaults)
359 return settings
359 return settings
360
360
361 def get_repo_settings(self, cache=False):
361 def get_repo_settings(self, cache=False):
362 """
362 """
363 Returns list of issue tracker settings per repository
363 Returns list of issue tracker settings per repository
364 """
364 """
365 if not self.repo_settings:
365 if not self.repo_settings:
366 raise Exception('Repository is not specified')
366 raise Exception('Repository is not specified')
367 all_settings = self.repo_settings.get_all_settings(cache=cache)
367 all_settings = self.repo_settings.get_all_settings(cache=cache)
368 settings = self._make_dict_for_settings(all_settings)
368 settings = self._make_dict_for_settings(all_settings)
369 return settings
369 return settings
370
370
371 def get_settings(self, cache=False):
371 def get_settings(self, cache=False):
372 if self.inherit_global_settings:
372 if self.inherit_global_settings:
373 return self.get_global_settings(cache=cache)
373 return self.get_global_settings(cache=cache)
374 else:
374 else:
375 return self.get_repo_settings(cache=cache)
375 return self.get_repo_settings(cache=cache)
376
376
377 def delete_entries(self, uid):
377 def delete_entries(self, uid):
378 if self.repo_settings:
378 if self.repo_settings:
379 all_patterns = self.get_repo_settings()
379 all_patterns = self.get_repo_settings()
380 settings_model = self.repo_settings
380 settings_model = self.repo_settings
381 else:
381 else:
382 all_patterns = self.get_global_settings()
382 all_patterns = self.get_global_settings()
383 settings_model = self.global_settings
383 settings_model = self.global_settings
384 entries = all_patterns.get(uid)
384 entries = all_patterns.get(uid)
385
385
386 for del_key in entries:
386 for del_key in entries:
387 setting_name = self._get_keyname(del_key, uid)
387 setting_name = self._get_keyname(del_key, uid)
388 entry = settings_model.get_setting_by_name(setting_name)
388 entry = settings_model.get_setting_by_name(setting_name)
389 if entry:
389 if entry:
390 Session().delete(entry)
390 Session().delete(entry)
391
391
392 Session().commit()
392 Session().commit()
393
393
394 def create_or_update_setting(
394 def create_or_update_setting(
395 self, name, val=Optional(''), type_=Optional('unicode')):
395 self, name, val=Optional(''), type_=Optional('unicode')):
396 if self.repo_settings:
396 if self.repo_settings:
397 setting = self.repo_settings.create_or_update_setting(
397 setting = self.repo_settings.create_or_update_setting(
398 name, val, type_)
398 name, val, type_)
399 else:
399 else:
400 setting = self.global_settings.create_or_update_setting(
400 setting = self.global_settings.create_or_update_setting(
401 name, val, type_)
401 name, val, type_)
402 return setting
402 return setting
403
403
404
404
405 class VcsSettingsModel(object):
405 class VcsSettingsModel(object):
406
406
407 INHERIT_SETTINGS = 'inherit_vcs_settings'
407 INHERIT_SETTINGS = 'inherit_vcs_settings'
408 GENERAL_SETTINGS = (
408 GENERAL_SETTINGS = (
409 'use_outdated_comments',
409 'use_outdated_comments',
410 'pr_merge_enabled',
410 'pr_merge_enabled',
411 'hg_use_rebase_for_merging',
411 'hg_use_rebase_for_merging',
412 'hg_close_branch_before_merging')
412 'hg_close_branch_before_merging',
413 'git_use_rebase_for_merging',
414 'git_close_branch_before_merging')
413
415
414 HOOKS_SETTINGS = (
416 HOOKS_SETTINGS = (
415 ('hooks', 'changegroup.repo_size'),
417 ('hooks', 'changegroup.repo_size'),
416 ('hooks', 'changegroup.push_logger'),
418 ('hooks', 'changegroup.push_logger'),
417 ('hooks', 'outgoing.pull_logger'),)
419 ('hooks', 'outgoing.pull_logger'),)
418 HG_SETTINGS = (
420 HG_SETTINGS = (
419 ('extensions', 'largefiles'),
421 ('extensions', 'largefiles'),
420 ('phases', 'publish'),
422 ('phases', 'publish'),
421 ('extensions', 'evolve'),)
423 ('extensions', 'evolve'),)
422 GIT_SETTINGS = (
424 GIT_SETTINGS = (
423 ('vcs_git_lfs', 'enabled'),)
425 ('vcs_git_lfs', 'enabled'),)
424 GLOBAL_HG_SETTINGS = (
426 GLOBAL_HG_SETTINGS = (
425 ('extensions', 'largefiles'),
427 ('extensions', 'largefiles'),
426 ('largefiles', 'usercache'),
428 ('largefiles', 'usercache'),
427 ('phases', 'publish'),
429 ('phases', 'publish'),
428 ('extensions', 'hgsubversion'),
430 ('extensions', 'hgsubversion'),
429 ('extensions', 'evolve'),)
431 ('extensions', 'evolve'),)
430 GLOBAL_GIT_SETTINGS = (
432 GLOBAL_GIT_SETTINGS = (
431 ('vcs_git_lfs', 'enabled'),
433 ('vcs_git_lfs', 'enabled'),
432 ('vcs_git_lfs', 'store_location'))
434 ('vcs_git_lfs', 'store_location'))
433 GLOBAL_SVN_SETTINGS = (
435 GLOBAL_SVN_SETTINGS = (
434 ('vcs_svn_proxy', 'http_requests_enabled'),
436 ('vcs_svn_proxy', 'http_requests_enabled'),
435 ('vcs_svn_proxy', 'http_server_url'))
437 ('vcs_svn_proxy', 'http_server_url'))
436
438
437 SVN_BRANCH_SECTION = 'vcs_svn_branch'
439 SVN_BRANCH_SECTION = 'vcs_svn_branch'
438 SVN_TAG_SECTION = 'vcs_svn_tag'
440 SVN_TAG_SECTION = 'vcs_svn_tag'
439 SSL_SETTING = ('web', 'push_ssl')
441 SSL_SETTING = ('web', 'push_ssl')
440 PATH_SETTING = ('paths', '/')
442 PATH_SETTING = ('paths', '/')
441
443
442 def __init__(self, sa=None, repo=None):
444 def __init__(self, sa=None, repo=None):
443 self.global_settings = SettingsModel(sa=sa)
445 self.global_settings = SettingsModel(sa=sa)
444 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
446 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
445 self._ui_settings = (
447 self._ui_settings = (
446 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
448 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
447 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
449 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
448
450
449 @property
451 @property
450 @assert_repo_settings
452 @assert_repo_settings
451 def inherit_global_settings(self):
453 def inherit_global_settings(self):
452 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
454 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
453 return setting.app_settings_value if setting else True
455 return setting.app_settings_value if setting else True
454
456
455 @inherit_global_settings.setter
457 @inherit_global_settings.setter
456 @assert_repo_settings
458 @assert_repo_settings
457 def inherit_global_settings(self, value):
459 def inherit_global_settings(self, value):
458 self.repo_settings.create_or_update_setting(
460 self.repo_settings.create_or_update_setting(
459 self.INHERIT_SETTINGS, value, type_='bool')
461 self.INHERIT_SETTINGS, value, type_='bool')
460
462
461 def get_global_svn_branch_patterns(self):
463 def get_global_svn_branch_patterns(self):
462 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
464 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
463
465
464 @assert_repo_settings
466 @assert_repo_settings
465 def get_repo_svn_branch_patterns(self):
467 def get_repo_svn_branch_patterns(self):
466 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
468 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
467
469
468 def get_global_svn_tag_patterns(self):
470 def get_global_svn_tag_patterns(self):
469 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
471 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
470
472
471 @assert_repo_settings
473 @assert_repo_settings
472 def get_repo_svn_tag_patterns(self):
474 def get_repo_svn_tag_patterns(self):
473 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
475 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
474
476
475 def get_global_settings(self):
477 def get_global_settings(self):
476 return self._collect_all_settings(global_=True)
478 return self._collect_all_settings(global_=True)
477
479
478 @assert_repo_settings
480 @assert_repo_settings
479 def get_repo_settings(self):
481 def get_repo_settings(self):
480 return self._collect_all_settings(global_=False)
482 return self._collect_all_settings(global_=False)
481
483
482 @assert_repo_settings
484 @assert_repo_settings
483 def create_or_update_repo_settings(
485 def create_or_update_repo_settings(
484 self, data, inherit_global_settings=False):
486 self, data, inherit_global_settings=False):
485 from rhodecode.model.scm import ScmModel
487 from rhodecode.model.scm import ScmModel
486
488
487 self.inherit_global_settings = inherit_global_settings
489 self.inherit_global_settings = inherit_global_settings
488
490
489 repo = self.repo_settings.get_repo()
491 repo = self.repo_settings.get_repo()
490 if not inherit_global_settings:
492 if not inherit_global_settings:
491 if repo.repo_type == 'svn':
493 if repo.repo_type == 'svn':
492 self.create_repo_svn_settings(data)
494 self.create_repo_svn_settings(data)
493 else:
495 else:
494 self.create_or_update_repo_hook_settings(data)
496 self.create_or_update_repo_hook_settings(data)
495 self.create_or_update_repo_pr_settings(data)
497 self.create_or_update_repo_pr_settings(data)
496
498
497 if repo.repo_type == 'hg':
499 if repo.repo_type == 'hg':
498 self.create_or_update_repo_hg_settings(data)
500 self.create_or_update_repo_hg_settings(data)
499
501
500 if repo.repo_type == 'git':
502 if repo.repo_type == 'git':
501 self.create_or_update_repo_git_settings(data)
503 self.create_or_update_repo_git_settings(data)
502
504
503 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
505 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
504
506
505 @assert_repo_settings
507 @assert_repo_settings
506 def create_or_update_repo_hook_settings(self, data):
508 def create_or_update_repo_hook_settings(self, data):
507 for section, key in self.HOOKS_SETTINGS:
509 for section, key in self.HOOKS_SETTINGS:
508 data_key = self._get_form_ui_key(section, key)
510 data_key = self._get_form_ui_key(section, key)
509 if data_key not in data:
511 if data_key not in data:
510 raise ValueError(
512 raise ValueError(
511 'The given data does not contain {} key'.format(data_key))
513 'The given data does not contain {} key'.format(data_key))
512
514
513 active = data.get(data_key)
515 active = data.get(data_key)
514 repo_setting = self.repo_settings.get_ui_by_section_and_key(
516 repo_setting = self.repo_settings.get_ui_by_section_and_key(
515 section, key)
517 section, key)
516 if not repo_setting:
518 if not repo_setting:
517 global_setting = self.global_settings.\
519 global_setting = self.global_settings.\
518 get_ui_by_section_and_key(section, key)
520 get_ui_by_section_and_key(section, key)
519 self.repo_settings.create_ui_section_value(
521 self.repo_settings.create_ui_section_value(
520 section, global_setting.ui_value, key=key, active=active)
522 section, global_setting.ui_value, key=key, active=active)
521 else:
523 else:
522 repo_setting.ui_active = active
524 repo_setting.ui_active = active
523 Session().add(repo_setting)
525 Session().add(repo_setting)
524
526
525 def update_global_hook_settings(self, data):
527 def update_global_hook_settings(self, data):
526 for section, key in self.HOOKS_SETTINGS:
528 for section, key in self.HOOKS_SETTINGS:
527 data_key = self._get_form_ui_key(section, key)
529 data_key = self._get_form_ui_key(section, key)
528 if data_key not in data:
530 if data_key not in data:
529 raise ValueError(
531 raise ValueError(
530 'The given data does not contain {} key'.format(data_key))
532 'The given data does not contain {} key'.format(data_key))
531 active = data.get(data_key)
533 active = data.get(data_key)
532 repo_setting = self.global_settings.get_ui_by_section_and_key(
534 repo_setting = self.global_settings.get_ui_by_section_and_key(
533 section, key)
535 section, key)
534 repo_setting.ui_active = active
536 repo_setting.ui_active = active
535 Session().add(repo_setting)
537 Session().add(repo_setting)
536
538
537 @assert_repo_settings
539 @assert_repo_settings
538 def create_or_update_repo_pr_settings(self, data):
540 def create_or_update_repo_pr_settings(self, data):
539 return self._create_or_update_general_settings(
541 return self._create_or_update_general_settings(
540 self.repo_settings, data)
542 self.repo_settings, data)
541
543
542 def create_or_update_global_pr_settings(self, data):
544 def create_or_update_global_pr_settings(self, data):
543 return self._create_or_update_general_settings(
545 return self._create_or_update_general_settings(
544 self.global_settings, data)
546 self.global_settings, data)
545
547
546 @assert_repo_settings
548 @assert_repo_settings
547 def create_repo_svn_settings(self, data):
549 def create_repo_svn_settings(self, data):
548 return self._create_svn_settings(self.repo_settings, data)
550 return self._create_svn_settings(self.repo_settings, data)
549
551
550 @assert_repo_settings
552 @assert_repo_settings
551 def create_or_update_repo_hg_settings(self, data):
553 def create_or_update_repo_hg_settings(self, data):
552 largefiles, phases, evolve = \
554 largefiles, phases, evolve = \
553 self.HG_SETTINGS
555 self.HG_SETTINGS
554 largefiles_key, phases_key, evolve_key = \
556 largefiles_key, phases_key, evolve_key = \
555 self._get_settings_keys(self.HG_SETTINGS, data)
557 self._get_settings_keys(self.HG_SETTINGS, data)
556
558
557 self._create_or_update_ui(
559 self._create_or_update_ui(
558 self.repo_settings, *largefiles, value='',
560 self.repo_settings, *largefiles, value='',
559 active=data[largefiles_key])
561 active=data[largefiles_key])
560 self._create_or_update_ui(
562 self._create_or_update_ui(
561 self.repo_settings, *evolve, value='',
563 self.repo_settings, *evolve, value='',
562 active=data[evolve_key])
564 active=data[evolve_key])
563 self._create_or_update_ui(
565 self._create_or_update_ui(
564 self.repo_settings, *phases, value=safe_str(data[phases_key]))
566 self.repo_settings, *phases, value=safe_str(data[phases_key]))
565
567
566 def create_or_update_global_hg_settings(self, data):
568 def create_or_update_global_hg_settings(self, data):
567 largefiles, largefiles_store, phases, hgsubversion, evolve \
569 largefiles, largefiles_store, phases, hgsubversion, evolve \
568 = self.GLOBAL_HG_SETTINGS
570 = self.GLOBAL_HG_SETTINGS
569 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
571 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
570 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data)
572 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data)
571
573
572 self._create_or_update_ui(
574 self._create_or_update_ui(
573 self.global_settings, *largefiles, value='',
575 self.global_settings, *largefiles, value='',
574 active=data[largefiles_key])
576 active=data[largefiles_key])
575 self._create_or_update_ui(
577 self._create_or_update_ui(
576 self.global_settings, *largefiles_store,
578 self.global_settings, *largefiles_store,
577 value=data[largefiles_store_key])
579 value=data[largefiles_store_key])
578 self._create_or_update_ui(
580 self._create_or_update_ui(
579 self.global_settings, *phases, value=safe_str(data[phases_key]))
581 self.global_settings, *phases, value=safe_str(data[phases_key]))
580 self._create_or_update_ui(
582 self._create_or_update_ui(
581 self.global_settings, *hgsubversion, active=data[subversion_key])
583 self.global_settings, *hgsubversion, active=data[subversion_key])
582 self._create_or_update_ui(
584 self._create_or_update_ui(
583 self.global_settings, *evolve, value='',
585 self.global_settings, *evolve, value='',
584 active=data[evolve_key])
586 active=data[evolve_key])
585
587
586 def create_or_update_repo_git_settings(self, data):
588 def create_or_update_repo_git_settings(self, data):
587 # NOTE(marcink): # comma make unpack work properly
589 # NOTE(marcink): # comma make unpack work properly
588 lfs_enabled, \
590 lfs_enabled, \
589 = self.GIT_SETTINGS
591 = self.GIT_SETTINGS
590
592
591 lfs_enabled_key, \
593 lfs_enabled_key, \
592 = self._get_settings_keys(self.GIT_SETTINGS, data)
594 = self._get_settings_keys(self.GIT_SETTINGS, data)
593
595
594 self._create_or_update_ui(
596 self._create_or_update_ui(
595 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
597 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
596 active=data[lfs_enabled_key])
598 active=data[lfs_enabled_key])
597
599
598 def create_or_update_global_git_settings(self, data):
600 def create_or_update_global_git_settings(self, data):
599 lfs_enabled, lfs_store_location \
601 lfs_enabled, lfs_store_location \
600 = self.GLOBAL_GIT_SETTINGS
602 = self.GLOBAL_GIT_SETTINGS
601 lfs_enabled_key, lfs_store_location_key \
603 lfs_enabled_key, lfs_store_location_key \
602 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
604 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
603
605
604 self._create_or_update_ui(
606 self._create_or_update_ui(
605 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
607 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
606 active=data[lfs_enabled_key])
608 active=data[lfs_enabled_key])
607 self._create_or_update_ui(
609 self._create_or_update_ui(
608 self.global_settings, *lfs_store_location,
610 self.global_settings, *lfs_store_location,
609 value=data[lfs_store_location_key])
611 value=data[lfs_store_location_key])
610
612
611 def create_or_update_global_svn_settings(self, data):
613 def create_or_update_global_svn_settings(self, data):
612 # branch/tags patterns
614 # branch/tags patterns
613 self._create_svn_settings(self.global_settings, data)
615 self._create_svn_settings(self.global_settings, data)
614
616
615 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
617 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
616 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
618 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
617 self.GLOBAL_SVN_SETTINGS, data)
619 self.GLOBAL_SVN_SETTINGS, data)
618
620
619 self._create_or_update_ui(
621 self._create_or_update_ui(
620 self.global_settings, *http_requests_enabled,
622 self.global_settings, *http_requests_enabled,
621 value=safe_str(data[http_requests_enabled_key]))
623 value=safe_str(data[http_requests_enabled_key]))
622 self._create_or_update_ui(
624 self._create_or_update_ui(
623 self.global_settings, *http_server_url,
625 self.global_settings, *http_server_url,
624 value=data[http_server_url_key])
626 value=data[http_server_url_key])
625
627
626 def update_global_ssl_setting(self, value):
628 def update_global_ssl_setting(self, value):
627 self._create_or_update_ui(
629 self._create_or_update_ui(
628 self.global_settings, *self.SSL_SETTING, value=value)
630 self.global_settings, *self.SSL_SETTING, value=value)
629
631
630 def update_global_path_setting(self, value):
632 def update_global_path_setting(self, value):
631 self._create_or_update_ui(
633 self._create_or_update_ui(
632 self.global_settings, *self.PATH_SETTING, value=value)
634 self.global_settings, *self.PATH_SETTING, value=value)
633
635
634 @assert_repo_settings
636 @assert_repo_settings
635 def delete_repo_svn_pattern(self, id_):
637 def delete_repo_svn_pattern(self, id_):
636 self.repo_settings.delete_ui(id_)
638 self.repo_settings.delete_ui(id_)
637
639
638 def delete_global_svn_pattern(self, id_):
640 def delete_global_svn_pattern(self, id_):
639 self.global_settings.delete_ui(id_)
641 self.global_settings.delete_ui(id_)
640
642
641 @assert_repo_settings
643 @assert_repo_settings
642 def get_repo_ui_settings(self, section=None, key=None):
644 def get_repo_ui_settings(self, section=None, key=None):
643 global_uis = self.global_settings.get_ui(section, key)
645 global_uis = self.global_settings.get_ui(section, key)
644 repo_uis = self.repo_settings.get_ui(section, key)
646 repo_uis = self.repo_settings.get_ui(section, key)
645 filtered_repo_uis = self._filter_ui_settings(repo_uis)
647 filtered_repo_uis = self._filter_ui_settings(repo_uis)
646 filtered_repo_uis_keys = [
648 filtered_repo_uis_keys = [
647 (s.section, s.key) for s in filtered_repo_uis]
649 (s.section, s.key) for s in filtered_repo_uis]
648
650
649 def _is_global_ui_filtered(ui):
651 def _is_global_ui_filtered(ui):
650 return (
652 return (
651 (ui.section, ui.key) in filtered_repo_uis_keys
653 (ui.section, ui.key) in filtered_repo_uis_keys
652 or ui.section in self._svn_sections)
654 or ui.section in self._svn_sections)
653
655
654 filtered_global_uis = [
656 filtered_global_uis = [
655 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
657 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
656
658
657 return filtered_global_uis + filtered_repo_uis
659 return filtered_global_uis + filtered_repo_uis
658
660
659 def get_global_ui_settings(self, section=None, key=None):
661 def get_global_ui_settings(self, section=None, key=None):
660 return self.global_settings.get_ui(section, key)
662 return self.global_settings.get_ui(section, key)
661
663
662 def get_ui_settings_as_config_obj(self, section=None, key=None):
664 def get_ui_settings_as_config_obj(self, section=None, key=None):
663 config = base.Config()
665 config = base.Config()
664
666
665 ui_settings = self.get_ui_settings(section=section, key=key)
667 ui_settings = self.get_ui_settings(section=section, key=key)
666
668
667 for entry in ui_settings:
669 for entry in ui_settings:
668 config.set(entry.section, entry.key, entry.value)
670 config.set(entry.section, entry.key, entry.value)
669
671
670 return config
672 return config
671
673
672 def get_ui_settings(self, section=None, key=None):
674 def get_ui_settings(self, section=None, key=None):
673 if not self.repo_settings or self.inherit_global_settings:
675 if not self.repo_settings or self.inherit_global_settings:
674 return self.get_global_ui_settings(section, key)
676 return self.get_global_ui_settings(section, key)
675 else:
677 else:
676 return self.get_repo_ui_settings(section, key)
678 return self.get_repo_ui_settings(section, key)
677
679
678 def get_svn_patterns(self, section=None):
680 def get_svn_patterns(self, section=None):
679 if not self.repo_settings:
681 if not self.repo_settings:
680 return self.get_global_ui_settings(section)
682 return self.get_global_ui_settings(section)
681 else:
683 else:
682 return self.get_repo_ui_settings(section)
684 return self.get_repo_ui_settings(section)
683
685
684 @assert_repo_settings
686 @assert_repo_settings
685 def get_repo_general_settings(self):
687 def get_repo_general_settings(self):
686 global_settings = self.global_settings.get_all_settings()
688 global_settings = self.global_settings.get_all_settings()
687 repo_settings = self.repo_settings.get_all_settings()
689 repo_settings = self.repo_settings.get_all_settings()
688 filtered_repo_settings = self._filter_general_settings(repo_settings)
690 filtered_repo_settings = self._filter_general_settings(repo_settings)
689 global_settings.update(filtered_repo_settings)
691 global_settings.update(filtered_repo_settings)
690 return global_settings
692 return global_settings
691
693
692 def get_global_general_settings(self):
694 def get_global_general_settings(self):
693 return self.global_settings.get_all_settings()
695 return self.global_settings.get_all_settings()
694
696
695 def get_general_settings(self):
697 def get_general_settings(self):
696 if not self.repo_settings or self.inherit_global_settings:
698 if not self.repo_settings or self.inherit_global_settings:
697 return self.get_global_general_settings()
699 return self.get_global_general_settings()
698 else:
700 else:
699 return self.get_repo_general_settings()
701 return self.get_repo_general_settings()
700
702
701 def get_repos_location(self):
703 def get_repos_location(self):
702 return self.global_settings.get_ui_by_key('/').ui_value
704 return self.global_settings.get_ui_by_key('/').ui_value
703
705
704 def _filter_ui_settings(self, settings):
706 def _filter_ui_settings(self, settings):
705 filtered_settings = [
707 filtered_settings = [
706 s for s in settings if self._should_keep_setting(s)]
708 s for s in settings if self._should_keep_setting(s)]
707 return filtered_settings
709 return filtered_settings
708
710
709 def _should_keep_setting(self, setting):
711 def _should_keep_setting(self, setting):
710 keep = (
712 keep = (
711 (setting.section, setting.key) in self._ui_settings or
713 (setting.section, setting.key) in self._ui_settings or
712 setting.section in self._svn_sections)
714 setting.section in self._svn_sections)
713 return keep
715 return keep
714
716
715 def _filter_general_settings(self, settings):
717 def _filter_general_settings(self, settings):
716 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
718 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
717 return {
719 return {
718 k: settings[k]
720 k: settings[k]
719 for k in settings if k in keys}
721 for k in settings if k in keys}
720
722
721 def _collect_all_settings(self, global_=False):
723 def _collect_all_settings(self, global_=False):
722 settings = self.global_settings if global_ else self.repo_settings
724 settings = self.global_settings if global_ else self.repo_settings
723 result = {}
725 result = {}
724
726
725 for section, key in self._ui_settings:
727 for section, key in self._ui_settings:
726 ui = settings.get_ui_by_section_and_key(section, key)
728 ui = settings.get_ui_by_section_and_key(section, key)
727 result_key = self._get_form_ui_key(section, key)
729 result_key = self._get_form_ui_key(section, key)
728
730
729 if ui:
731 if ui:
730 if section in ('hooks', 'extensions'):
732 if section in ('hooks', 'extensions'):
731 result[result_key] = ui.ui_active
733 result[result_key] = ui.ui_active
732 elif result_key in ['vcs_git_lfs_enabled']:
734 elif result_key in ['vcs_git_lfs_enabled']:
733 result[result_key] = ui.ui_active
735 result[result_key] = ui.ui_active
734 else:
736 else:
735 result[result_key] = ui.ui_value
737 result[result_key] = ui.ui_value
736
738
737 for name in self.GENERAL_SETTINGS:
739 for name in self.GENERAL_SETTINGS:
738 setting = settings.get_setting_by_name(name)
740 setting = settings.get_setting_by_name(name)
739 if setting:
741 if setting:
740 result_key = 'rhodecode_{}'.format(name)
742 result_key = 'rhodecode_{}'.format(name)
741 result[result_key] = setting.app_settings_value
743 result[result_key] = setting.app_settings_value
742
744
743 return result
745 return result
744
746
745 def _get_form_ui_key(self, section, key):
747 def _get_form_ui_key(self, section, key):
746 return '{section}_{key}'.format(
748 return '{section}_{key}'.format(
747 section=section, key=key.replace('.', '_'))
749 section=section, key=key.replace('.', '_'))
748
750
749 def _create_or_update_ui(
751 def _create_or_update_ui(
750 self, settings, section, key, value=None, active=None):
752 self, settings, section, key, value=None, active=None):
751 ui = settings.get_ui_by_section_and_key(section, key)
753 ui = settings.get_ui_by_section_and_key(section, key)
752 if not ui:
754 if not ui:
753 active = True if active is None else active
755 active = True if active is None else active
754 settings.create_ui_section_value(
756 settings.create_ui_section_value(
755 section, value, key=key, active=active)
757 section, value, key=key, active=active)
756 else:
758 else:
757 if active is not None:
759 if active is not None:
758 ui.ui_active = active
760 ui.ui_active = active
759 if value is not None:
761 if value is not None:
760 ui.ui_value = value
762 ui.ui_value = value
761 Session().add(ui)
763 Session().add(ui)
762
764
763 def _create_svn_settings(self, settings, data):
765 def _create_svn_settings(self, settings, data):
764 svn_settings = {
766 svn_settings = {
765 'new_svn_branch': self.SVN_BRANCH_SECTION,
767 'new_svn_branch': self.SVN_BRANCH_SECTION,
766 'new_svn_tag': self.SVN_TAG_SECTION
768 'new_svn_tag': self.SVN_TAG_SECTION
767 }
769 }
768 for key in svn_settings:
770 for key in svn_settings:
769 if data.get(key):
771 if data.get(key):
770 settings.create_ui_section_value(svn_settings[key], data[key])
772 settings.create_ui_section_value(svn_settings[key], data[key])
771
773
772 def _create_or_update_general_settings(self, settings, data):
774 def _create_or_update_general_settings(self, settings, data):
773 for name in self.GENERAL_SETTINGS:
775 for name in self.GENERAL_SETTINGS:
774 data_key = 'rhodecode_{}'.format(name)
776 data_key = 'rhodecode_{}'.format(name)
775 if data_key not in data:
777 if data_key not in data:
776 raise ValueError(
778 raise ValueError(
777 'The given data does not contain {} key'.format(data_key))
779 'The given data does not contain {} key'.format(data_key))
778 setting = settings.create_or_update_setting(
780 setting = settings.create_or_update_setting(
779 name, data[data_key], 'bool')
781 name, data[data_key], 'bool')
780 Session().add(setting)
782 Session().add(setting)
781
783
782 def _get_settings_keys(self, settings, data):
784 def _get_settings_keys(self, settings, data):
783 data_keys = [self._get_form_ui_key(*s) for s in settings]
785 data_keys = [self._get_form_ui_key(*s) for s in settings]
784 for data_key in data_keys:
786 for data_key in data_keys:
785 if data_key not in data:
787 if data_key not in data:
786 raise ValueError(
788 raise ValueError(
787 'The given data does not contain {} key'.format(data_key))
789 'The given data does not contain {} key'.format(data_key))
788 return data_keys
790 return data_keys
789
791
790 def create_largeobjects_dirs_if_needed(self, repo_store_path):
792 def create_largeobjects_dirs_if_needed(self, repo_store_path):
791 """
793 """
792 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
794 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
793 does a repository scan if enabled in the settings.
795 does a repository scan if enabled in the settings.
794 """
796 """
795
797
796 from rhodecode.lib.vcs.backends.hg import largefiles_store
798 from rhodecode.lib.vcs.backends.hg import largefiles_store
797 from rhodecode.lib.vcs.backends.git import lfs_store
799 from rhodecode.lib.vcs.backends.git import lfs_store
798
800
799 paths = [
801 paths = [
800 largefiles_store(repo_store_path),
802 largefiles_store(repo_store_path),
801 lfs_store(repo_store_path)]
803 lfs_store(repo_store_path)]
802
804
803 for path in paths:
805 for path in paths:
804 if os.path.isdir(path):
806 if os.path.isdir(path):
805 continue
807 continue
806 if os.path.isfile(path):
808 if os.path.isfile(path):
807 continue
809 continue
808 # not a file nor dir, we try to create it
810 # not a file nor dir, we try to create it
809 try:
811 try:
810 os.makedirs(path)
812 os.makedirs(path)
811 except Exception:
813 except Exception:
812 log.warning('Failed to create largefiles dir:%s', path)
814 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,343 +1,370 b''
1 ## snippet for displaying vcs settings
1 ## snippet for displaying vcs settings
2 ## usage:
2 ## usage:
3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
3 ## <%namespace name="vcss" file="/base/vcssettings.mako"/>
4 ## ${vcss.vcs_settings_fields()}
4 ## ${vcss.vcs_settings_fields()}
5
5
6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, allow_repo_location_change=False, **kwargs)">
6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, allow_repo_location_change=False, **kwargs)">
7 % if display_globals:
7 % if display_globals:
8 <div class="panel panel-default">
8 <div class="panel panel-default">
9 <div class="panel-heading" id="general">
9 <div class="panel-heading" id="general">
10 <h3 class="panel-title">${_('General')}<a class="permalink" href="#general"> ΒΆ</a></h3>
10 <h3 class="panel-title">${_('General')}<a class="permalink" href="#general"> ΒΆ</a></h3>
11 </div>
11 </div>
12 <div class="panel-body">
12 <div class="panel-body">
13 <div class="field">
13 <div class="field">
14 <div class="checkbox">
14 <div class="checkbox">
15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
17 </div>
17 </div>
18 <div class="label">
18 <div class="label">
19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
20 </div>
20 </div>
21 </div>
21 </div>
22 </div>
22 </div>
23 </div>
23 </div>
24 % endif
24 % endif
25
25
26 % if display_globals:
26 % if display_globals:
27 <div class="panel panel-default">
27 <div class="panel panel-default">
28 <div class="panel-heading" id="vcs-storage-options">
28 <div class="panel-heading" id="vcs-storage-options">
29 <h3 class="panel-title">${_('Main Storage Location')}<a class="permalink" href="#vcs-storage-options"> ΒΆ</a></h3>
29 <h3 class="panel-title">${_('Main Storage Location')}<a class="permalink" href="#vcs-storage-options"> ΒΆ</a></h3>
30 </div>
30 </div>
31 <div class="panel-body">
31 <div class="panel-body">
32 <div class="field">
32 <div class="field">
33 <div class="inputx locked_input">
33 <div class="inputx locked_input">
34 %if allow_repo_location_change:
34 %if allow_repo_location_change:
35 ${h.text('paths_root_path',size=59,readonly="readonly", class_="disabled")}
35 ${h.text('paths_root_path',size=59,readonly="readonly", class_="disabled")}
36 <span id="path_unlock" class="tooltip"
36 <span id="path_unlock" class="tooltip"
37 title="${h.tooltip(_('Click to unlock. You must restart RhodeCode in order to make this setting take effect.'))}">
37 title="${h.tooltip(_('Click to unlock. You must restart RhodeCode in order to make this setting take effect.'))}">
38 <div class="btn btn-default lock_input_button"><i id="path_unlock_icon" class="icon-lock"></i></div>
38 <div class="btn btn-default lock_input_button"><i id="path_unlock_icon" class="icon-lock"></i></div>
39 </span>
39 </span>
40 %else:
40 %else:
41 ${_('Repository location change is disabled. You can enable this by changing the `allow_repo_location_change` inside .ini file.')}
41 ${_('Repository location change is disabled. You can enable this by changing the `allow_repo_location_change` inside .ini file.')}
42 ## form still requires this but we cannot internally change it anyway
42 ## form still requires this but we cannot internally change it anyway
43 ${h.hidden('paths_root_path',size=30,readonly="readonly", class_="disabled")}
43 ${h.hidden('paths_root_path',size=30,readonly="readonly", class_="disabled")}
44 %endif
44 %endif
45 </div>
45 </div>
46 </div>
46 </div>
47 <div class="label">
47 <div class="label">
48 <span class="help-block">${_('Filesystem location where repositories should be stored. After changing this value a restart and rescan of the repository folder are required.')}</span>
48 <span class="help-block">${_('Filesystem location where repositories should be stored. After changing this value a restart and rescan of the repository folder are required.')}</span>
49 </div>
49 </div>
50 </div>
50 </div>
51 </div>
51 </div>
52 % endif
52 % endif
53
53
54 % if display_globals or repo_type in ['git', 'hg']:
54 % if display_globals or repo_type in ['git', 'hg']:
55 <div class="panel panel-default">
55 <div class="panel panel-default">
56 <div class="panel-heading" id="vcs-hooks-options">
56 <div class="panel-heading" id="vcs-hooks-options">
57 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"> ΒΆ</a></h3>
57 <h3 class="panel-title">${_('Internal Hooks')}<a class="permalink" href="#vcs-hooks-options"> ΒΆ</a></h3>
58 </div>
58 </div>
59 <div class="panel-body">
59 <div class="panel-body">
60 <div class="field">
60 <div class="field">
61 <div class="checkbox">
61 <div class="checkbox">
62 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
62 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
63 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
63 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
64 </div>
64 </div>
65
65
66 <div class="label">
66 <div class="label">
67 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
67 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
68 </div>
68 </div>
69 <div class="checkbox">
69 <div class="checkbox">
70 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
70 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
71 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
71 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
72 </div>
72 </div>
73 <div class="label">
73 <div class="label">
74 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
74 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
75 </div>
75 </div>
76 <div class="checkbox">
76 <div class="checkbox">
77 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
77 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
78 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
78 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
79 </div>
79 </div>
80 <div class="label">
80 <div class="label">
81 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
81 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
82 </div>
82 </div>
83 </div>
83 </div>
84 </div>
84 </div>
85 </div>
85 </div>
86 % endif
86 % endif
87
87
88 % if display_globals or repo_type in ['hg']:
88 % if display_globals or repo_type in ['hg']:
89 <div class="panel panel-default">
89 <div class="panel panel-default">
90 <div class="panel-heading" id="vcs-hg-options">
90 <div class="panel-heading" id="vcs-hg-options">
91 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"> ΒΆ</a></h3>
91 <h3 class="panel-title">${_('Mercurial Settings')}<a class="permalink" href="#vcs-hg-options"> ΒΆ</a></h3>
92 </div>
92 </div>
93 <div class="panel-body">
93 <div class="panel-body">
94 <div class="checkbox">
94 <div class="checkbox">
95 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
95 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
96 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
96 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
97 </div>
97 </div>
98 <div class="label">
98 <div class="label">
99 % if display_globals:
99 % if display_globals:
100 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
100 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
101 % else:
101 % else:
102 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
102 <span class="help-block">${_('Enable Largefiles extensions for this repository.')}</span>
103 % endif
103 % endif
104 </div>
104 </div>
105
105
106 % if display_globals:
106 % if display_globals:
107 <div class="field">
107 <div class="field">
108 <div class="input">
108 <div class="input">
109 ${h.text('largefiles_usercache' + suffix, size=59)}
109 ${h.text('largefiles_usercache' + suffix, size=59)}
110 </div>
110 </div>
111 </div>
111 </div>
112 <div class="label">
112 <div class="label">
113 <span class="help-block">${_('Filesystem location where Mercurial largefile objects should be stored.')}</span>
113 <span class="help-block">${_('Filesystem location where Mercurial largefile objects should be stored.')}</span>
114 </div>
114 </div>
115 % endif
115 % endif
116
116
117 <div class="checkbox">
117 <div class="checkbox">
118 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
118 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
119 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
119 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
120 </div>
120 </div>
121 <div class="label">
121 <div class="label">
122 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
122 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
123 </div>
123 </div>
124 % if display_globals:
124 % if display_globals:
125 <div class="checkbox">
125 <div class="checkbox">
126 ${h.checkbox('extensions_hgsubversion' + suffix,'True')}
126 ${h.checkbox('extensions_hgsubversion' + suffix,'True')}
127 <label for="extensions_hgsubversion${suffix}">${_('Enable hgsubversion extension')}</label>
127 <label for="extensions_hgsubversion${suffix}">${_('Enable hgsubversion extension')}</label>
128 </div>
128 </div>
129 <div class="label">
129 <div class="label">
130 <span class="help-block">${_('Requires hgsubversion library to be installed. Allows cloning remote SVN repositories and migrates them to Mercurial type.')}</span>
130 <span class="help-block">${_('Requires hgsubversion library to be installed. Allows cloning remote SVN repositories and migrates them to Mercurial type.')}</span>
131 </div>
131 </div>
132 % endif
132 % endif
133
133
134 <div class="checkbox">
134 <div class="checkbox">
135 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
135 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
136 <label for="extensions_evolve${suffix}">${_('Enable evolve extension')}</label>
136 <label for="extensions_evolve${suffix}">${_('Enable evolve extension')}</label>
137 </div>
137 </div>
138 <div class="label">
138 <div class="label">
139 % if display_globals:
139 % if display_globals:
140 <span class="help-block">${_('Enable evolve extension for all repositories.')}</span>
140 <span class="help-block">${_('Enable evolve extension for all repositories.')}</span>
141 % else:
141 % else:
142 <span class="help-block">${_('Enable evolve extension for this repository.')}</span>
142 <span class="help-block">${_('Enable evolve extension for this repository.')}</span>
143 % endif
143 % endif
144 </div>
144 </div>
145
145
146 </div>
146 </div>
147 </div>
147 </div>
148 ## LABS for HG
149 % if c.labs_active:
150 <div class="panel panel-danger">
151 <div class="panel-heading">
152 <h3 class="panel-title">${_('Mercurial Labs Settings')} (${_('These features are considered experimental and may not work as expected.')})</h3>
153 </div>
154 <div class="panel-body">
155
156 <div class="checkbox">
157 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
158 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
159 </div>
160 <div class="label">
161 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
162 </div>
163
164 <div class="checkbox">
165 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
166 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
167 </div>
168 <div class="label">
169 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
170 </div>
171
172 </div>
173 </div>
174 % endif
175
176 % endif
148 % endif
177
149
178 % if display_globals or repo_type in ['git']:
150 % if display_globals or repo_type in ['git']:
179 <div class="panel panel-default">
151 <div class="panel panel-default">
180 <div class="panel-heading" id="vcs-git-options">
152 <div class="panel-heading" id="vcs-git-options">
181 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"> ΒΆ</a></h3>
153 <h3 class="panel-title">${_('Git Settings')}<a class="permalink" href="#vcs-git-options"> ΒΆ</a></h3>
182 </div>
154 </div>
183 <div class="panel-body">
155 <div class="panel-body">
184 <div class="checkbox">
156 <div class="checkbox">
185 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
157 ${h.checkbox('vcs_git_lfs_enabled' + suffix, 'True', **kwargs)}
186 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
158 <label for="vcs_git_lfs_enabled${suffix}">${_('Enable lfs extension')}</label>
187 </div>
159 </div>
188 <div class="label">
160 <div class="label">
189 % if display_globals:
161 % if display_globals:
190 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
162 <span class="help-block">${_('Enable lfs extensions for all repositories.')}</span>
191 % else:
163 % else:
192 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
164 <span class="help-block">${_('Enable lfs extensions for this repository.')}</span>
193 % endif
165 % endif
194 </div>
166 </div>
195
167
196 % if display_globals:
168 % if display_globals:
197 <div class="field">
169 <div class="field">
198 <div class="input">
170 <div class="input">
199 ${h.text('vcs_git_lfs_store_location' + suffix, size=59)}
171 ${h.text('vcs_git_lfs_store_location' + suffix, size=59)}
200 </div>
172 </div>
201 </div>
173 </div>
202 <div class="label">
174 <div class="label">
203 <span class="help-block">${_('Filesystem location where Git lfs objects should be stored.')}</span>
175 <span class="help-block">${_('Filesystem location where Git lfs objects should be stored.')}</span>
204 </div>
176 </div>
205 % endif
177 % endif
206 </div>
178 </div>
207 </div>
179 </div>
208 % endif
180 % endif
209
181
210
182
211 % if display_globals:
183 % if display_globals:
212 <div class="panel panel-default">
184 <div class="panel panel-default">
213 <div class="panel-heading" id="vcs-global-svn-options">
185 <div class="panel-heading" id="vcs-global-svn-options">
214 <h3 class="panel-title">${_('Global Subversion Settings')}<a class="permalink" href="#vcs-global-svn-options"> ΒΆ</a></h3>
186 <h3 class="panel-title">${_('Global Subversion Settings')}<a class="permalink" href="#vcs-global-svn-options"> ΒΆ</a></h3>
215 </div>
187 </div>
216 <div class="panel-body">
188 <div class="panel-body">
217 <div class="field">
189 <div class="field">
218 <div class="checkbox">
190 <div class="checkbox">
219 ${h.checkbox('vcs_svn_proxy_http_requests_enabled' + suffix, 'True', **kwargs)}
191 ${h.checkbox('vcs_svn_proxy_http_requests_enabled' + suffix, 'True', **kwargs)}
220 <label for="vcs_svn_proxy_http_requests_enabled${suffix}">${_('Proxy subversion HTTP requests')}</label>
192 <label for="vcs_svn_proxy_http_requests_enabled${suffix}">${_('Proxy subversion HTTP requests')}</label>
221 </div>
193 </div>
222 <div class="label">
194 <div class="label">
223 <span class="help-block">
195 <span class="help-block">
224 ${_('Subversion HTTP Support. Enables communication with SVN over HTTP protocol.')}
196 ${_('Subversion HTTP Support. Enables communication with SVN over HTTP protocol.')}
225 <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('SVN Protocol setup Documentation')}</a>.
197 <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('SVN Protocol setup Documentation')}</a>.
226 </span>
198 </span>
227 </div>
199 </div>
228 </div>
200 </div>
229 <div class="field">
201 <div class="field">
230 <div class="label">
202 <div class="label">
231 <label for="vcs_svn_proxy_http_server_url">${_('Subversion HTTP Server URL')}</label><br/>
203 <label for="vcs_svn_proxy_http_server_url">${_('Subversion HTTP Server URL')}</label><br/>
232 </div>
204 </div>
233 <div class="input">
205 <div class="input">
234 ${h.text('vcs_svn_proxy_http_server_url',size=59)}
206 ${h.text('vcs_svn_proxy_http_server_url',size=59)}
235 % if c.svn_proxy_generate_config:
207 % if c.svn_proxy_generate_config:
236 <span class="buttons">
208 <span class="buttons">
237 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Generate Apache Config')}</button>
209 <button class="btn btn-primary" id="vcs_svn_generate_cfg">${_('Generate Apache Config')}</button>
238 </span>
210 </span>
239 % endif
211 % endif
240 </div>
212 </div>
241 </div>
213 </div>
242 </div>
214 </div>
243 </div>
215 </div>
244 % endif
216 % endif
245
217
246 % if display_globals or repo_type in ['svn']:
218 % if display_globals or repo_type in ['svn']:
247 <div class="panel panel-default">
219 <div class="panel panel-default">
248 <div class="panel-heading" id="vcs-svn-options">
220 <div class="panel-heading" id="vcs-svn-options">
249 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ΒΆ</a></h3>
221 <h3 class="panel-title">${_('Subversion Settings')}<a class="permalink" href="#vcs-svn-options"> ΒΆ</a></h3>
250 </div>
222 </div>
251 <div class="panel-body">
223 <div class="panel-body">
252 <div class="field">
224 <div class="field">
253 <div class="content" >
225 <div class="content" >
254 <label>${_('Repository patterns')}</label><br/>
226 <label>${_('Repository patterns')}</label><br/>
255 </div>
227 </div>
256 </div>
228 </div>
257 <div class="label">
229 <div class="label">
258 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
230 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
259 </div>
231 </div>
260
232
261 <div class="field branch_patterns">
233 <div class="field branch_patterns">
262 <div class="input" >
234 <div class="input" >
263 <label>${_('Branches')}:</label><br/>
235 <label>${_('Branches')}:</label><br/>
264 </div>
236 </div>
265 % if svn_branch_patterns:
237 % if svn_branch_patterns:
266 % for branch in svn_branch_patterns:
238 % for branch in svn_branch_patterns:
267 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
239 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
268 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
240 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
269 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
241 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
270 % if kwargs.get('disabled') != 'disabled':
242 % if kwargs.get('disabled') != 'disabled':
271 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
243 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
272 ${_('Delete')}
244 ${_('Delete')}
273 </span>
245 </span>
274 % endif
246 % endif
275 </div>
247 </div>
276 % endfor
248 % endfor
277 %endif
249 %endif
278 </div>
250 </div>
279 % if kwargs.get('disabled') != 'disabled':
251 % if kwargs.get('disabled') != 'disabled':
280 <div class="field branch_patterns">
252 <div class="field branch_patterns">
281 <div class="input" >
253 <div class="input" >
282 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
254 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
283 </div>
255 </div>
284 </div>
256 </div>
285 % endif
257 % endif
286 <div class="field tag_patterns">
258 <div class="field tag_patterns">
287 <div class="input" >
259 <div class="input" >
288 <label>${_('Tags')}:</label><br/>
260 <label>${_('Tags')}:</label><br/>
289 </div>
261 </div>
290 % if svn_tag_patterns:
262 % if svn_tag_patterns:
291 % for tag in svn_tag_patterns:
263 % for tag in svn_tag_patterns:
292 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
264 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
293 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
265 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
294 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
266 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
295 % if kwargs.get('disabled') != 'disabled':
267 % if kwargs.get('disabled') != 'disabled':
296 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
268 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
297 ${_('Delete')}
269 ${_('Delete')}
298 </span>
270 </span>
299 %endif
271 %endif
300 </div>
272 </div>
301 % endfor
273 % endfor
302 % endif
274 % endif
303 </div>
275 </div>
304 % if kwargs.get('disabled') != 'disabled':
276 % if kwargs.get('disabled') != 'disabled':
305 <div class="field tag_patterns">
277 <div class="field tag_patterns">
306 <div class="input" >
278 <div class="input" >
307 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
279 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
308 </div>
280 </div>
309 </div>
281 </div>
310 %endif
282 %endif
311 </div>
283 </div>
312 </div>
284 </div>
313 % else:
285 % else:
314 ${h.hidden('new_svn_branch' + suffix, '')}
286 ${h.hidden('new_svn_branch' + suffix, '')}
315 ${h.hidden('new_svn_tag' + suffix, '')}
287 ${h.hidden('new_svn_tag' + suffix, '')}
316 % endif
288 % endif
317
289
318
290
319 % if display_globals or repo_type in ['hg', 'git']:
291 % if display_globals or repo_type in ['hg', 'git']:
320 <div class="panel panel-default">
292 <div class="panel panel-default">
321 <div class="panel-heading" id="vcs-pull-requests-options">
293 <div class="panel-heading" id="vcs-pull-requests-options">
322 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
294 <h3 class="panel-title">${_('Pull Request Settings')}<a class="permalink" href="#vcs-pull-requests-options"> ΒΆ</a></h3>
323 </div>
295 </div>
324 <div class="panel-body">
296 <div class="panel-body">
325 <div class="checkbox">
297 <div class="checkbox">
326 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
298 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
327 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
299 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
328 </div>
300 </div>
329 <div class="label">
301 <div class="label">
330 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
302 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
331 </div>
303 </div>
332 <div class="checkbox">
304 <div class="checkbox">
333 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
305 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
334 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
306 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
335 </div>
307 </div>
336 <div class="label">
308 <div class="label">
337 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
309 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
338 </div>
310 </div>
339 </div>
311 </div>
340 </div>
312 </div>
341 % endif
313 % endif
342
314
315 % if display_globals or repo_type in ['hg',]:
316 <div class="panel panel-default">
317 <div class="panel-heading" id="vcs-pull-requests-options">
318 <h3 class="panel-title">${_('Mercurial Pull Request Settings')}<a class="permalink" href="#vcs-hg-pull-requests-options"> ΒΆ</a></h3>
319 </div>
320 <div class="panel-body">
321 ## Specific HG settings
322 <div class="checkbox">
323 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
324 <label for="rhodecode_hg_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
325 </div>
326 <div class="label">
327 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
328 </div>
329
330 <div class="checkbox">
331 ${h.checkbox('rhodecode_hg_close_branch_before_merging' + suffix, 'True', **kwargs)}
332 <label for="rhodecode_hg_close_branch_before_merging{suffix}">${_('Close branch before merging it')}</label>
333 </div>
334 <div class="label">
335 <span class="help-block">${_('Close branch before merging it into destination branch. No effect when rebase strategy is use.')}</span>
336 </div>
337
338
339 </div>
340 </div>
341 % endif
342
343 ## DISABLED FOR GIT FOR NOW as the rebase/close is not supported yet
344 ## % if display_globals or repo_type in ['git']:
345 ## <div class="panel panel-default">
346 ## <div class="panel-heading" id="vcs-pull-requests-options">
347 ## <h3 class="panel-title">${_('Git Pull Request Settings')}<a class="permalink" href="#vcs-git-pull-requests-options"> ΒΆ</a></h3>
348 ## </div>
349 ## <div class="panel-body">
350 ## <div class="checkbox">
351 ## ${h.checkbox('rhodecode_git_use_rebase_for_merging' + suffix, 'True', **kwargs)}
352 ## <label for="rhodecode_git_use_rebase_for_merging${suffix}">${_('Use rebase as merge strategy')}</label>
353 ## </div>
354 ## <div class="label">
355 ## <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
356 ## </div>
357 ##
358 ## <div class="checkbox">
359 ## ${h.checkbox('rhodecode_git_close_branch_before_merging' + suffix, 'True', **kwargs)}
360 ## <label for="rhodecode_git_close_branch_before_merging{suffix}">${_('Delete branch after merging it')}</label>
361 ## </div>
362 ## <div class="label">
363 ## <span class="help-block">${_('Delete branch after merging it into destination branch. No effect when rebase strategy is use.')}</span>
364 ## </div>
365 ## </div>
366 ## </div>
367 ## % endif
368
369
343 </%def>
370 </%def>
@@ -1,682 +1,670 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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 mock
21 import mock
22 import pytest
22 import pytest
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.config.routing import ADMIN_PREFIX
25 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.lib.utils2 import md5
26 from rhodecode.lib.utils2 import md5
27 from rhodecode.model.db import RhodeCodeUi
27 from rhodecode.model.db import RhodeCodeUi
28 from rhodecode.model.meta import Session
28 from rhodecode.model.meta import Session
29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 from rhodecode.tests import url, assert_session_flash
30 from rhodecode.tests import url, assert_session_flash
31 from rhodecode.tests.utils import AssertResponse
31 from rhodecode.tests.utils import AssertResponse
32
32
33
33
34 UPDATE_DATA_QUALNAME = (
34 UPDATE_DATA_QUALNAME = (
35 'rhodecode.apps.admin.views.system_info.AdminSystemInfoSettingsView.get_update_data')
35 'rhodecode.apps.admin.views.system_info.AdminSystemInfoSettingsView.get_update_data')
36
36
37
37
38 @pytest.mark.usefixtures('autologin_user', 'app')
38 @pytest.mark.usefixtures('autologin_user', 'app')
39 class TestAdminSettingsController(object):
39 class TestAdminSettingsController(object):
40
40
41 @pytest.mark.parametrize('urlname', [
41 @pytest.mark.parametrize('urlname', [
42 'admin_settings_vcs',
42 'admin_settings_vcs',
43 'admin_settings_mapping',
43 'admin_settings_mapping',
44 'admin_settings_global',
44 'admin_settings_global',
45 'admin_settings_visual',
45 'admin_settings_visual',
46 'admin_settings_email',
46 'admin_settings_email',
47 'admin_settings_hooks',
47 'admin_settings_hooks',
48 'admin_settings_search',
48 'admin_settings_search',
49 ])
49 ])
50 def test_simple_get(self, urlname, app):
50 def test_simple_get(self, urlname, app):
51 app.get(url(urlname))
51 app.get(url(urlname))
52
52
53 def test_create_custom_hook(self, csrf_token):
53 def test_create_custom_hook(self, csrf_token):
54 response = self.app.post(
54 response = self.app.post(
55 url('admin_settings_hooks'),
55 url('admin_settings_hooks'),
56 params={
56 params={
57 'new_hook_ui_key': 'test_hooks_1',
57 'new_hook_ui_key': 'test_hooks_1',
58 'new_hook_ui_value': 'cd /tmp',
58 'new_hook_ui_value': 'cd /tmp',
59 'csrf_token': csrf_token})
59 'csrf_token': csrf_token})
60
60
61 response = response.follow()
61 response = response.follow()
62 response.mustcontain('test_hooks_1')
62 response.mustcontain('test_hooks_1')
63 response.mustcontain('cd /tmp')
63 response.mustcontain('cd /tmp')
64
64
65 def test_create_custom_hook_delete(self, csrf_token):
65 def test_create_custom_hook_delete(self, csrf_token):
66 response = self.app.post(
66 response = self.app.post(
67 url('admin_settings_hooks'),
67 url('admin_settings_hooks'),
68 params={
68 params={
69 'new_hook_ui_key': 'test_hooks_2',
69 'new_hook_ui_key': 'test_hooks_2',
70 'new_hook_ui_value': 'cd /tmp2',
70 'new_hook_ui_value': 'cd /tmp2',
71 'csrf_token': csrf_token})
71 'csrf_token': csrf_token})
72
72
73 response = response.follow()
73 response = response.follow()
74 response.mustcontain('test_hooks_2')
74 response.mustcontain('test_hooks_2')
75 response.mustcontain('cd /tmp2')
75 response.mustcontain('cd /tmp2')
76
76
77 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
77 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
78
78
79 # delete
79 # delete
80 self.app.post(
80 self.app.post(
81 url('admin_settings_hooks'),
81 url('admin_settings_hooks'),
82 params={'hook_id': hook_id, 'csrf_token': csrf_token})
82 params={'hook_id': hook_id, 'csrf_token': csrf_token})
83 response = self.app.get(url('admin_settings_hooks'))
83 response = self.app.get(url('admin_settings_hooks'))
84 response.mustcontain(no=['test_hooks_2'])
84 response.mustcontain(no=['test_hooks_2'])
85 response.mustcontain(no=['cd /tmp2'])
85 response.mustcontain(no=['cd /tmp2'])
86
86
87
87
88 @pytest.mark.usefixtures('autologin_user', 'app')
88 @pytest.mark.usefixtures('autologin_user', 'app')
89 class TestAdminSettingsGlobal(object):
89 class TestAdminSettingsGlobal(object):
90
90
91 def test_pre_post_code_code_active(self, csrf_token):
91 def test_pre_post_code_code_active(self, csrf_token):
92 pre_code = 'rc-pre-code-187652122'
92 pre_code = 'rc-pre-code-187652122'
93 post_code = 'rc-postcode-98165231'
93 post_code = 'rc-postcode-98165231'
94
94
95 response = self.post_and_verify_settings({
95 response = self.post_and_verify_settings({
96 'rhodecode_pre_code': pre_code,
96 'rhodecode_pre_code': pre_code,
97 'rhodecode_post_code': post_code,
97 'rhodecode_post_code': post_code,
98 'csrf_token': csrf_token,
98 'csrf_token': csrf_token,
99 })
99 })
100
100
101 response = response.follow()
101 response = response.follow()
102 response.mustcontain(pre_code, post_code)
102 response.mustcontain(pre_code, post_code)
103
103
104 def test_pre_post_code_code_inactive(self, csrf_token):
104 def test_pre_post_code_code_inactive(self, csrf_token):
105 pre_code = 'rc-pre-code-187652122'
105 pre_code = 'rc-pre-code-187652122'
106 post_code = 'rc-postcode-98165231'
106 post_code = 'rc-postcode-98165231'
107 response = self.post_and_verify_settings({
107 response = self.post_and_verify_settings({
108 'rhodecode_pre_code': '',
108 'rhodecode_pre_code': '',
109 'rhodecode_post_code': '',
109 'rhodecode_post_code': '',
110 'csrf_token': csrf_token,
110 'csrf_token': csrf_token,
111 })
111 })
112
112
113 response = response.follow()
113 response = response.follow()
114 response.mustcontain(no=[pre_code, post_code])
114 response.mustcontain(no=[pre_code, post_code])
115
115
116 def test_captcha_activate(self, csrf_token):
116 def test_captcha_activate(self, csrf_token):
117 self.post_and_verify_settings({
117 self.post_and_verify_settings({
118 'rhodecode_captcha_private_key': '1234567890',
118 'rhodecode_captcha_private_key': '1234567890',
119 'rhodecode_captcha_public_key': '1234567890',
119 'rhodecode_captcha_public_key': '1234567890',
120 'csrf_token': csrf_token,
120 'csrf_token': csrf_token,
121 })
121 })
122
122
123 response = self.app.get(ADMIN_PREFIX + '/register')
123 response = self.app.get(ADMIN_PREFIX + '/register')
124 response.mustcontain('captcha')
124 response.mustcontain('captcha')
125
125
126 def test_captcha_deactivate(self, csrf_token):
126 def test_captcha_deactivate(self, csrf_token):
127 self.post_and_verify_settings({
127 self.post_and_verify_settings({
128 'rhodecode_captcha_private_key': '',
128 'rhodecode_captcha_private_key': '',
129 'rhodecode_captcha_public_key': '1234567890',
129 'rhodecode_captcha_public_key': '1234567890',
130 'csrf_token': csrf_token,
130 'csrf_token': csrf_token,
131 })
131 })
132
132
133 response = self.app.get(ADMIN_PREFIX + '/register')
133 response = self.app.get(ADMIN_PREFIX + '/register')
134 response.mustcontain(no=['captcha'])
134 response.mustcontain(no=['captcha'])
135
135
136 def test_title_change(self, csrf_token):
136 def test_title_change(self, csrf_token):
137 old_title = 'RhodeCode'
137 old_title = 'RhodeCode'
138 new_title = old_title + '_changed'
138 new_title = old_title + '_changed'
139
139
140 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
140 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
141 response = self.post_and_verify_settings({
141 response = self.post_and_verify_settings({
142 'rhodecode_title': new_title,
142 'rhodecode_title': new_title,
143 'csrf_token': csrf_token,
143 'csrf_token': csrf_token,
144 })
144 })
145
145
146 response = response.follow()
146 response = response.follow()
147 response.mustcontain(
147 response.mustcontain(
148 """<div class="branding">- %s</div>""" % new_title)
148 """<div class="branding">- %s</div>""" % new_title)
149
149
150 def post_and_verify_settings(self, settings):
150 def post_and_verify_settings(self, settings):
151 old_title = 'RhodeCode'
151 old_title = 'RhodeCode'
152 old_realm = 'RhodeCode authentication'
152 old_realm = 'RhodeCode authentication'
153 params = {
153 params = {
154 'rhodecode_title': old_title,
154 'rhodecode_title': old_title,
155 'rhodecode_realm': old_realm,
155 'rhodecode_realm': old_realm,
156 'rhodecode_pre_code': '',
156 'rhodecode_pre_code': '',
157 'rhodecode_post_code': '',
157 'rhodecode_post_code': '',
158 'rhodecode_captcha_private_key': '',
158 'rhodecode_captcha_private_key': '',
159 'rhodecode_captcha_public_key': '',
159 'rhodecode_captcha_public_key': '',
160 'rhodecode_create_personal_repo_group': False,
160 'rhodecode_create_personal_repo_group': False,
161 'rhodecode_personal_repo_group_pattern': '${username}',
161 'rhodecode_personal_repo_group_pattern': '${username}',
162 }
162 }
163 params.update(settings)
163 params.update(settings)
164 response = self.app.post(url('admin_settings_global'), params=params)
164 response = self.app.post(url('admin_settings_global'), params=params)
165
165
166 assert_session_flash(response, 'Updated application settings')
166 assert_session_flash(response, 'Updated application settings')
167 app_settings = SettingsModel().get_all_settings()
167 app_settings = SettingsModel().get_all_settings()
168 del settings['csrf_token']
168 del settings['csrf_token']
169 for key, value in settings.iteritems():
169 for key, value in settings.iteritems():
170 assert app_settings[key] == value.decode('utf-8')
170 assert app_settings[key] == value.decode('utf-8')
171
171
172 return response
172 return response
173
173
174
174
175 @pytest.mark.usefixtures('autologin_user', 'app')
175 @pytest.mark.usefixtures('autologin_user', 'app')
176 class TestAdminSettingsVcs(object):
176 class TestAdminSettingsVcs(object):
177
177
178 def test_contains_svn_default_patterns(self, app):
178 def test_contains_svn_default_patterns(self, app):
179 response = app.get(url('admin_settings_vcs'))
179 response = app.get(url('admin_settings_vcs'))
180 expected_patterns = [
180 expected_patterns = [
181 '/trunk',
181 '/trunk',
182 '/branches/*',
182 '/branches/*',
183 '/tags/*',
183 '/tags/*',
184 ]
184 ]
185 for pattern in expected_patterns:
185 for pattern in expected_patterns:
186 response.mustcontain(pattern)
186 response.mustcontain(pattern)
187
187
188 def test_add_new_svn_branch_and_tag_pattern(
188 def test_add_new_svn_branch_and_tag_pattern(
189 self, app, backend_svn, form_defaults, disable_sql_cache,
189 self, app, backend_svn, form_defaults, disable_sql_cache,
190 csrf_token):
190 csrf_token):
191 form_defaults.update({
191 form_defaults.update({
192 'new_svn_branch': '/exp/branches/*',
192 'new_svn_branch': '/exp/branches/*',
193 'new_svn_tag': '/important_tags/*',
193 'new_svn_tag': '/important_tags/*',
194 'csrf_token': csrf_token,
194 'csrf_token': csrf_token,
195 })
195 })
196
196
197 response = app.post(
197 response = app.post(
198 url('admin_settings_vcs'), params=form_defaults, status=302)
198 url('admin_settings_vcs'), params=form_defaults, status=302)
199 response = response.follow()
199 response = response.follow()
200
200
201 # Expect to find the new values on the page
201 # Expect to find the new values on the page
202 response.mustcontain('/exp/branches/*')
202 response.mustcontain('/exp/branches/*')
203 response.mustcontain('/important_tags/*')
203 response.mustcontain('/important_tags/*')
204
204
205 # Expect that those patterns are used to match branches and tags now
205 # Expect that those patterns are used to match branches and tags now
206 repo = backend_svn['svn-simple-layout'].scm_instance()
206 repo = backend_svn['svn-simple-layout'].scm_instance()
207 assert 'exp/branches/exp-sphinx-docs' in repo.branches
207 assert 'exp/branches/exp-sphinx-docs' in repo.branches
208 assert 'important_tags/v0.5' in repo.tags
208 assert 'important_tags/v0.5' in repo.tags
209
209
210 def test_add_same_svn_value_twice_shows_an_error_message(
210 def test_add_same_svn_value_twice_shows_an_error_message(
211 self, app, form_defaults, csrf_token, settings_util):
211 self, app, form_defaults, csrf_token, settings_util):
212 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
212 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
213 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
213 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
214
214
215 response = app.post(
215 response = app.post(
216 url('admin_settings_vcs'),
216 url('admin_settings_vcs'),
217 params={
217 params={
218 'paths_root_path': form_defaults['paths_root_path'],
218 'paths_root_path': form_defaults['paths_root_path'],
219 'new_svn_branch': '/test',
219 'new_svn_branch': '/test',
220 'new_svn_tag': '/test',
220 'new_svn_tag': '/test',
221 'csrf_token': csrf_token,
221 'csrf_token': csrf_token,
222 },
222 },
223 status=200)
223 status=200)
224
224
225 response.mustcontain("Pattern already exists")
225 response.mustcontain("Pattern already exists")
226 response.mustcontain("Some form inputs contain invalid data.")
226 response.mustcontain("Some form inputs contain invalid data.")
227
227
228 @pytest.mark.parametrize('section', [
228 @pytest.mark.parametrize('section', [
229 'vcs_svn_branch',
229 'vcs_svn_branch',
230 'vcs_svn_tag',
230 'vcs_svn_tag',
231 ])
231 ])
232 def test_delete_svn_patterns(
232 def test_delete_svn_patterns(
233 self, section, app, csrf_token, settings_util):
233 self, section, app, csrf_token, settings_util):
234 setting = settings_util.create_rhodecode_ui(
234 setting = settings_util.create_rhodecode_ui(
235 section, '/test_delete', cleanup=False)
235 section, '/test_delete', cleanup=False)
236
236
237 app.post(
237 app.post(
238 url('admin_settings_vcs'),
238 url('admin_settings_vcs'),
239 params={
239 params={
240 '_method': 'delete',
240 '_method': 'delete',
241 'delete_svn_pattern': setting.ui_id,
241 'delete_svn_pattern': setting.ui_id,
242 'csrf_token': csrf_token},
242 'csrf_token': csrf_token},
243 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
243 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
244
244
245 @pytest.mark.parametrize('section', [
245 @pytest.mark.parametrize('section', [
246 'vcs_svn_branch',
246 'vcs_svn_branch',
247 'vcs_svn_tag',
247 'vcs_svn_tag',
248 ])
248 ])
249 def test_delete_svn_patterns_raises_400_when_no_xhr(
249 def test_delete_svn_patterns_raises_400_when_no_xhr(
250 self, section, app, csrf_token, settings_util):
250 self, section, app, csrf_token, settings_util):
251 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
251 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
252
252
253 app.post(
253 app.post(
254 url('admin_settings_vcs'),
254 url('admin_settings_vcs'),
255 params={
255 params={
256 '_method': 'delete',
256 '_method': 'delete',
257 'delete_svn_pattern': setting.ui_id,
257 'delete_svn_pattern': setting.ui_id,
258 'csrf_token': csrf_token},
258 'csrf_token': csrf_token},
259 status=400)
259 status=400)
260
260
261 def test_extensions_hgsubversion(self, app, form_defaults, csrf_token):
261 def test_extensions_hgsubversion(self, app, form_defaults, csrf_token):
262 form_defaults.update({
262 form_defaults.update({
263 'csrf_token': csrf_token,
263 'csrf_token': csrf_token,
264 'extensions_hgsubversion': 'True',
264 'extensions_hgsubversion': 'True',
265 })
265 })
266 response = app.post(
266 response = app.post(
267 url('admin_settings_vcs'),
267 url('admin_settings_vcs'),
268 params=form_defaults,
268 params=form_defaults,
269 status=302)
269 status=302)
270
270
271 response = response.follow()
271 response = response.follow()
272 extensions_input = (
272 extensions_input = (
273 '<input id="extensions_hgsubversion" '
273 '<input id="extensions_hgsubversion" '
274 'name="extensions_hgsubversion" type="checkbox" '
274 'name="extensions_hgsubversion" type="checkbox" '
275 'value="True" checked="checked" />')
275 'value="True" checked="checked" />')
276 response.mustcontain(extensions_input)
276 response.mustcontain(extensions_input)
277
277
278 def test_extensions_hgevolve(self, app, form_defaults, csrf_token):
278 def test_extensions_hgevolve(self, app, form_defaults, csrf_token):
279 form_defaults.update({
279 form_defaults.update({
280 'csrf_token': csrf_token,
280 'csrf_token': csrf_token,
281 'extensions_evolve': 'True',
281 'extensions_evolve': 'True',
282 })
282 })
283 response = app.post(
283 response = app.post(
284 url('admin_settings_vcs'),
284 url('admin_settings_vcs'),
285 params=form_defaults,
285 params=form_defaults,
286 status=302)
286 status=302)
287
287
288 response = response.follow()
288 response = response.follow()
289 extensions_input = (
289 extensions_input = (
290 '<input id="extensions_evolve" '
290 '<input id="extensions_evolve" '
291 'name="extensions_evolve" type="checkbox" '
291 'name="extensions_evolve" type="checkbox" '
292 'value="True" checked="checked" />')
292 'value="True" checked="checked" />')
293 response.mustcontain(extensions_input)
293 response.mustcontain(extensions_input)
294
294
295 def test_has_a_section_for_pull_request_settings(self, app):
295 def test_has_a_section_for_pull_request_settings(self, app):
296 response = app.get(url('admin_settings_vcs'))
296 response = app.get(url('admin_settings_vcs'))
297 response.mustcontain('Pull Request Settings')
297 response.mustcontain('Pull Request Settings')
298
298
299 def test_has_an_input_for_invalidation_of_inline_comments(
299 def test_has_an_input_for_invalidation_of_inline_comments(
300 self, app):
300 self, app):
301 response = app.get(url('admin_settings_vcs'))
301 response = app.get(url('admin_settings_vcs'))
302 assert_response = AssertResponse(response)
302 assert_response = AssertResponse(response)
303 assert_response.one_element_exists(
303 assert_response.one_element_exists(
304 '[name=rhodecode_use_outdated_comments]')
304 '[name=rhodecode_use_outdated_comments]')
305
305
306 @pytest.mark.parametrize('new_value', [True, False])
306 @pytest.mark.parametrize('new_value', [True, False])
307 def test_allows_to_change_invalidation_of_inline_comments(
307 def test_allows_to_change_invalidation_of_inline_comments(
308 self, app, form_defaults, csrf_token, new_value):
308 self, app, form_defaults, csrf_token, new_value):
309 setting_key = 'use_outdated_comments'
309 setting_key = 'use_outdated_comments'
310 setting = SettingsModel().create_or_update_setting(
310 setting = SettingsModel().create_or_update_setting(
311 setting_key, not new_value, 'bool')
311 setting_key, not new_value, 'bool')
312 Session().add(setting)
312 Session().add(setting)
313 Session().commit()
313 Session().commit()
314
314
315 form_defaults.update({
315 form_defaults.update({
316 'csrf_token': csrf_token,
316 'csrf_token': csrf_token,
317 'rhodecode_use_outdated_comments': str(new_value),
317 'rhodecode_use_outdated_comments': str(new_value),
318 })
318 })
319 response = app.post(
319 response = app.post(
320 url('admin_settings_vcs'),
320 url('admin_settings_vcs'),
321 params=form_defaults,
321 params=form_defaults,
322 status=302)
322 status=302)
323 response = response.follow()
323 response = response.follow()
324 setting = SettingsModel().get_setting_by_name(setting_key)
324 setting = SettingsModel().get_setting_by_name(setting_key)
325 assert setting.app_settings_value is new_value
325 assert setting.app_settings_value is new_value
326
326
327 def test_has_a_section_for_labs_settings_if_enabled(self, app):
328 with mock.patch.dict(
329 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
330 response = self.app.get(url('admin_settings_vcs'))
331 response.mustcontain('Labs Settings')
332
333 def test_has_not_a_section_for_labs_settings_if_disables(self, app):
334 with mock.patch.dict(
335 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
336 response = self.app.get(url('admin_settings_vcs'))
337 response.mustcontain(no='Labs Settings')
338
339 @pytest.mark.parametrize('new_value', [True, False])
327 @pytest.mark.parametrize('new_value', [True, False])
340 def test_allows_to_change_hg_rebase_merge_strategy(
328 def test_allows_to_change_hg_rebase_merge_strategy(
341 self, app, form_defaults, csrf_token, new_value):
329 self, app, form_defaults, csrf_token, new_value):
342 setting_key = 'hg_use_rebase_for_merging'
330 setting_key = 'hg_use_rebase_for_merging'
343
331
344 form_defaults.update({
332 form_defaults.update({
345 'csrf_token': csrf_token,
333 'csrf_token': csrf_token,
346 'rhodecode_' + setting_key: str(new_value),
334 'rhodecode_' + setting_key: str(new_value),
347 })
335 })
348
336
349 with mock.patch.dict(
337 with mock.patch.dict(
350 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
338 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
351 app.post(
339 app.post(
352 url('admin_settings_vcs'),
340 url('admin_settings_vcs'),
353 params=form_defaults,
341 params=form_defaults,
354 status=302)
342 status=302)
355
343
356 setting = SettingsModel().get_setting_by_name(setting_key)
344 setting = SettingsModel().get_setting_by_name(setting_key)
357 assert setting.app_settings_value is new_value
345 assert setting.app_settings_value is new_value
358
346
359 @pytest.fixture
347 @pytest.fixture
360 def disable_sql_cache(self, request):
348 def disable_sql_cache(self, request):
361 patcher = mock.patch(
349 patcher = mock.patch(
362 'rhodecode.lib.caching_query.FromCache.process_query')
350 'rhodecode.lib.caching_query.FromCache.process_query')
363 request.addfinalizer(patcher.stop)
351 request.addfinalizer(patcher.stop)
364 patcher.start()
352 patcher.start()
365
353
366 @pytest.fixture
354 @pytest.fixture
367 def form_defaults(self):
355 def form_defaults(self):
368 from rhodecode.controllers.admin.settings import SettingsController
356 from rhodecode.controllers.admin.settings import SettingsController
369 controller = SettingsController()
357 controller = SettingsController()
370 return controller._form_defaults()
358 return controller._form_defaults()
371
359
372 # TODO: johbo: What we really want is to checkpoint before a test run and
360 # TODO: johbo: What we really want is to checkpoint before a test run and
373 # reset the session afterwards.
361 # reset the session afterwards.
374 @pytest.fixture(scope='class', autouse=True)
362 @pytest.fixture(scope='class', autouse=True)
375 def cleanup_settings(self, request, pylonsapp):
363 def cleanup_settings(self, request, pylonsapp):
376 ui_id = RhodeCodeUi.ui_id
364 ui_id = RhodeCodeUi.ui_id
377 original_ids = list(
365 original_ids = list(
378 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
366 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
379
367
380 @request.addfinalizer
368 @request.addfinalizer
381 def cleanup():
369 def cleanup():
382 RhodeCodeUi.query().filter(
370 RhodeCodeUi.query().filter(
383 ui_id.notin_(original_ids)).delete(False)
371 ui_id.notin_(original_ids)).delete(False)
384
372
385
373
386 @pytest.mark.usefixtures('autologin_user', 'app')
374 @pytest.mark.usefixtures('autologin_user', 'app')
387 class TestLabsSettings(object):
375 class TestLabsSettings(object):
388 def test_get_settings_page_disabled(self):
376 def test_get_settings_page_disabled(self):
389 with mock.patch.dict(rhodecode.CONFIG,
377 with mock.patch.dict(rhodecode.CONFIG,
390 {'labs_settings_active': 'false'}):
378 {'labs_settings_active': 'false'}):
391 response = self.app.get(url('admin_settings_labs'), status=302)
379 response = self.app.get(url('admin_settings_labs'), status=302)
392
380
393 assert response.location.endswith(url('admin_settings'))
381 assert response.location.endswith(url('admin_settings'))
394
382
395 def test_get_settings_page_enabled(self):
383 def test_get_settings_page_enabled(self):
396 from rhodecode.controllers.admin import settings
384 from rhodecode.controllers.admin import settings
397 lab_settings = [
385 lab_settings = [
398 settings.LabSetting(
386 settings.LabSetting(
399 key='rhodecode_bool',
387 key='rhodecode_bool',
400 type='bool',
388 type='bool',
401 group='bool group',
389 group='bool group',
402 label='bool label',
390 label='bool label',
403 help='bool help'
391 help='bool help'
404 ),
392 ),
405 settings.LabSetting(
393 settings.LabSetting(
406 key='rhodecode_text',
394 key='rhodecode_text',
407 type='unicode',
395 type='unicode',
408 group='text group',
396 group='text group',
409 label='text label',
397 label='text label',
410 help='text help'
398 help='text help'
411 ),
399 ),
412 ]
400 ]
413 with mock.patch.dict(rhodecode.CONFIG,
401 with mock.patch.dict(rhodecode.CONFIG,
414 {'labs_settings_active': 'true'}):
402 {'labs_settings_active': 'true'}):
415 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
403 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
416 response = self.app.get(url('admin_settings_labs'))
404 response = self.app.get(url('admin_settings_labs'))
417
405
418 assert '<label>bool group:</label>' in response
406 assert '<label>bool group:</label>' in response
419 assert '<label for="rhodecode_bool">bool label</label>' in response
407 assert '<label for="rhodecode_bool">bool label</label>' in response
420 assert '<p class="help-block">bool help</p>' in response
408 assert '<p class="help-block">bool help</p>' in response
421 assert 'name="rhodecode_bool" type="checkbox"' in response
409 assert 'name="rhodecode_bool" type="checkbox"' in response
422
410
423 assert '<label>text group:</label>' in response
411 assert '<label>text group:</label>' in response
424 assert '<label for="rhodecode_text">text label</label>' in response
412 assert '<label for="rhodecode_text">text label</label>' in response
425 assert '<p class="help-block">text help</p>' in response
413 assert '<p class="help-block">text help</p>' in response
426 assert 'name="rhodecode_text" size="60" type="text"' in response
414 assert 'name="rhodecode_text" size="60" type="text"' in response
427
415
428
416
429 @pytest.mark.usefixtures('app')
417 @pytest.mark.usefixtures('app')
430 class TestOpenSourceLicenses(object):
418 class TestOpenSourceLicenses(object):
431
419
432 def _get_url(self):
420 def _get_url(self):
433 return ADMIN_PREFIX + '/settings/open_source'
421 return ADMIN_PREFIX + '/settings/open_source'
434
422
435 def test_records_are_displayed(self, autologin_user):
423 def test_records_are_displayed(self, autologin_user):
436 sample_licenses = {
424 sample_licenses = {
437 "python2.7-pytest-2.7.1": {
425 "python2.7-pytest-2.7.1": {
438 "UNKNOWN": None
426 "UNKNOWN": None
439 },
427 },
440 "python2.7-Markdown-2.6.2": {
428 "python2.7-Markdown-2.6.2": {
441 "BSD-3-Clause": "http://spdx.org/licenses/BSD-3-Clause"
429 "BSD-3-Clause": "http://spdx.org/licenses/BSD-3-Clause"
442 }
430 }
443 }
431 }
444 read_licenses_patch = mock.patch(
432 read_licenses_patch = mock.patch(
445 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
433 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
446 return_value=sample_licenses)
434 return_value=sample_licenses)
447 with read_licenses_patch:
435 with read_licenses_patch:
448 response = self.app.get(self._get_url(), status=200)
436 response = self.app.get(self._get_url(), status=200)
449
437
450 assert_response = AssertResponse(response)
438 assert_response = AssertResponse(response)
451 assert_response.element_contains(
439 assert_response.element_contains(
452 '.panel-heading', 'Licenses of Third Party Packages')
440 '.panel-heading', 'Licenses of Third Party Packages')
453 for name in sample_licenses:
441 for name in sample_licenses:
454 response.mustcontain(name)
442 response.mustcontain(name)
455 for license in sample_licenses[name]:
443 for license in sample_licenses[name]:
456 assert_response.element_contains('.panel-body', license)
444 assert_response.element_contains('.panel-body', license)
457
445
458 def test_records_can_be_read(self, autologin_user):
446 def test_records_can_be_read(self, autologin_user):
459 response = self.app.get(self._get_url(), status=200)
447 response = self.app.get(self._get_url(), status=200)
460 assert_response = AssertResponse(response)
448 assert_response = AssertResponse(response)
461 assert_response.element_contains(
449 assert_response.element_contains(
462 '.panel-heading', 'Licenses of Third Party Packages')
450 '.panel-heading', 'Licenses of Third Party Packages')
463
451
464 def test_forbidden_when_normal_user(self, autologin_regular_user):
452 def test_forbidden_when_normal_user(self, autologin_regular_user):
465 self.app.get(self._get_url(), status=404)
453 self.app.get(self._get_url(), status=404)
466
454
467
455
468 @pytest.mark.usefixtures('app')
456 @pytest.mark.usefixtures('app')
469 class TestUserSessions(object):
457 class TestUserSessions(object):
470
458
471 def _get_url(self, name='admin_settings_sessions'):
459 def _get_url(self, name='admin_settings_sessions'):
472 return {
460 return {
473 'admin_settings_sessions': ADMIN_PREFIX + '/settings/sessions',
461 'admin_settings_sessions': ADMIN_PREFIX + '/settings/sessions',
474 'admin_settings_sessions_cleanup': ADMIN_PREFIX + '/settings/sessions/cleanup'
462 'admin_settings_sessions_cleanup': ADMIN_PREFIX + '/settings/sessions/cleanup'
475 }[name]
463 }[name]
476
464
477 def test_forbidden_when_normal_user(self, autologin_regular_user):
465 def test_forbidden_when_normal_user(self, autologin_regular_user):
478 self.app.get(self._get_url(), status=404)
466 self.app.get(self._get_url(), status=404)
479
467
480 def test_show_sessions_page(self, autologin_user):
468 def test_show_sessions_page(self, autologin_user):
481 response = self.app.get(self._get_url(), status=200)
469 response = self.app.get(self._get_url(), status=200)
482 response.mustcontain('file')
470 response.mustcontain('file')
483
471
484 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
472 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
485
473
486 post_data = {
474 post_data = {
487 'csrf_token': csrf_token,
475 'csrf_token': csrf_token,
488 'expire_days': '60'
476 'expire_days': '60'
489 }
477 }
490 response = self.app.post(
478 response = self.app.post(
491 self._get_url('admin_settings_sessions_cleanup'), params=post_data,
479 self._get_url('admin_settings_sessions_cleanup'), params=post_data,
492 status=302)
480 status=302)
493 assert_session_flash(response, 'Cleaned up old sessions')
481 assert_session_flash(response, 'Cleaned up old sessions')
494
482
495
483
496 @pytest.mark.usefixtures('app')
484 @pytest.mark.usefixtures('app')
497 class TestAdminSystemInfo(object):
485 class TestAdminSystemInfo(object):
498 def _get_url(self, name='admin_settings_system'):
486 def _get_url(self, name='admin_settings_system'):
499 return {
487 return {
500 'admin_settings_system': ADMIN_PREFIX + '/settings/system',
488 'admin_settings_system': ADMIN_PREFIX + '/settings/system',
501 'admin_settings_system_update': ADMIN_PREFIX + '/settings/system/updates',
489 'admin_settings_system_update': ADMIN_PREFIX + '/settings/system/updates',
502 }[name]
490 }[name]
503
491
504 def test_forbidden_when_normal_user(self, autologin_regular_user):
492 def test_forbidden_when_normal_user(self, autologin_regular_user):
505 self.app.get(self._get_url(), status=404)
493 self.app.get(self._get_url(), status=404)
506
494
507 def test_system_info_page(self, autologin_user):
495 def test_system_info_page(self, autologin_user):
508 response = self.app.get(self._get_url())
496 response = self.app.get(self._get_url())
509 response.mustcontain('RhodeCode Community Edition, version {}'.format(
497 response.mustcontain('RhodeCode Community Edition, version {}'.format(
510 rhodecode.__version__))
498 rhodecode.__version__))
511
499
512 def test_system_update_new_version(self, autologin_user):
500 def test_system_update_new_version(self, autologin_user):
513 update_data = {
501 update_data = {
514 'versions': [
502 'versions': [
515 {
503 {
516 'version': '100.3.1415926535',
504 'version': '100.3.1415926535',
517 'general': 'The latest version we are ever going to ship'
505 'general': 'The latest version we are ever going to ship'
518 },
506 },
519 {
507 {
520 'version': '0.0.0',
508 'version': '0.0.0',
521 'general': 'The first version we ever shipped'
509 'general': 'The first version we ever shipped'
522 }
510 }
523 ]
511 ]
524 }
512 }
525 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
513 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
526 response = self.app.get(self._get_url('admin_settings_system_update'))
514 response = self.app.get(self._get_url('admin_settings_system_update'))
527 response.mustcontain('A <b>new version</b> is available')
515 response.mustcontain('A <b>new version</b> is available')
528
516
529 def test_system_update_nothing_new(self, autologin_user):
517 def test_system_update_nothing_new(self, autologin_user):
530 update_data = {
518 update_data = {
531 'versions': [
519 'versions': [
532 {
520 {
533 'version': '0.0.0',
521 'version': '0.0.0',
534 'general': 'The first version we ever shipped'
522 'general': 'The first version we ever shipped'
535 }
523 }
536 ]
524 ]
537 }
525 }
538 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
526 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
539 response = self.app.get(self._get_url('admin_settings_system_update'))
527 response = self.app.get(self._get_url('admin_settings_system_update'))
540 response.mustcontain(
528 response.mustcontain(
541 'You already have the <b>latest</b> stable version.')
529 'You already have the <b>latest</b> stable version.')
542
530
543 def test_system_update_bad_response(self, autologin_user):
531 def test_system_update_bad_response(self, autologin_user):
544 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
532 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
545 response = self.app.get(self._get_url('admin_settings_system_update'))
533 response = self.app.get(self._get_url('admin_settings_system_update'))
546 response.mustcontain(
534 response.mustcontain(
547 'Bad data sent from update server')
535 'Bad data sent from update server')
548
536
549
537
550 @pytest.mark.usefixtures("app")
538 @pytest.mark.usefixtures("app")
551 class TestAdminSettingsIssueTracker(object):
539 class TestAdminSettingsIssueTracker(object):
552 RC_PREFIX = 'rhodecode_'
540 RC_PREFIX = 'rhodecode_'
553 SHORT_PATTERN_KEY = 'issuetracker_pat_'
541 SHORT_PATTERN_KEY = 'issuetracker_pat_'
554 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
542 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
555
543
556 def test_issuetracker_index(self, autologin_user):
544 def test_issuetracker_index(self, autologin_user):
557 response = self.app.get(url('admin_settings_issuetracker'))
545 response = self.app.get(url('admin_settings_issuetracker'))
558 assert response.status_code == 200
546 assert response.status_code == 200
559
547
560 def test_add_empty_issuetracker_pattern(
548 def test_add_empty_issuetracker_pattern(
561 self, request, autologin_user, csrf_token):
549 self, request, autologin_user, csrf_token):
562 post_url = url('admin_settings_issuetracker_save')
550 post_url = url('admin_settings_issuetracker_save')
563 post_data = {
551 post_data = {
564 'csrf_token': csrf_token
552 'csrf_token': csrf_token
565 }
553 }
566 self.app.post(post_url, post_data, status=302)
554 self.app.post(post_url, post_data, status=302)
567
555
568 def test_add_issuetracker_pattern(
556 def test_add_issuetracker_pattern(
569 self, request, autologin_user, csrf_token):
557 self, request, autologin_user, csrf_token):
570 pattern = 'issuetracker_pat'
558 pattern = 'issuetracker_pat'
571 another_pattern = pattern+'1'
559 another_pattern = pattern+'1'
572 post_url = url('admin_settings_issuetracker_save')
560 post_url = url('admin_settings_issuetracker_save')
573 post_data = {
561 post_data = {
574 'new_pattern_pattern_0': pattern,
562 'new_pattern_pattern_0': pattern,
575 'new_pattern_url_0': 'url',
563 'new_pattern_url_0': 'url',
576 'new_pattern_prefix_0': 'prefix',
564 'new_pattern_prefix_0': 'prefix',
577 'new_pattern_description_0': 'description',
565 'new_pattern_description_0': 'description',
578 'new_pattern_pattern_1': another_pattern,
566 'new_pattern_pattern_1': another_pattern,
579 'new_pattern_url_1': 'url1',
567 'new_pattern_url_1': 'url1',
580 'new_pattern_prefix_1': 'prefix1',
568 'new_pattern_prefix_1': 'prefix1',
581 'new_pattern_description_1': 'description1',
569 'new_pattern_description_1': 'description1',
582 'csrf_token': csrf_token
570 'csrf_token': csrf_token
583 }
571 }
584 self.app.post(post_url, post_data, status=302)
572 self.app.post(post_url, post_data, status=302)
585 settings = SettingsModel().get_all_settings()
573 settings = SettingsModel().get_all_settings()
586 self.uid = md5(pattern)
574 self.uid = md5(pattern)
587 assert settings[self.PATTERN_KEY+self.uid] == pattern
575 assert settings[self.PATTERN_KEY+self.uid] == pattern
588 self.another_uid = md5(another_pattern)
576 self.another_uid = md5(another_pattern)
589 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
577 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
590
578
591 @request.addfinalizer
579 @request.addfinalizer
592 def cleanup():
580 def cleanup():
593 defaults = SettingsModel().get_all_settings()
581 defaults = SettingsModel().get_all_settings()
594
582
595 entries = [name for name in defaults if (
583 entries = [name for name in defaults if (
596 (self.uid in name) or (self.another_uid) in name)]
584 (self.uid in name) or (self.another_uid) in name)]
597 start = len(self.RC_PREFIX)
585 start = len(self.RC_PREFIX)
598 for del_key in entries:
586 for del_key in entries:
599 # TODO: anderson: get_by_name needs name without prefix
587 # TODO: anderson: get_by_name needs name without prefix
600 entry = SettingsModel().get_setting_by_name(del_key[start:])
588 entry = SettingsModel().get_setting_by_name(del_key[start:])
601 Session().delete(entry)
589 Session().delete(entry)
602
590
603 Session().commit()
591 Session().commit()
604
592
605 def test_edit_issuetracker_pattern(
593 def test_edit_issuetracker_pattern(
606 self, autologin_user, backend, csrf_token, request):
594 self, autologin_user, backend, csrf_token, request):
607 old_pattern = 'issuetracker_pat'
595 old_pattern = 'issuetracker_pat'
608 old_uid = md5(old_pattern)
596 old_uid = md5(old_pattern)
609 pattern = 'issuetracker_pat_new'
597 pattern = 'issuetracker_pat_new'
610 self.new_uid = md5(pattern)
598 self.new_uid = md5(pattern)
611
599
612 SettingsModel().create_or_update_setting(
600 SettingsModel().create_or_update_setting(
613 self.SHORT_PATTERN_KEY+old_uid, old_pattern, 'unicode')
601 self.SHORT_PATTERN_KEY+old_uid, old_pattern, 'unicode')
614
602
615 post_url = url('admin_settings_issuetracker_save')
603 post_url = url('admin_settings_issuetracker_save')
616 post_data = {
604 post_data = {
617 'new_pattern_pattern_0': pattern,
605 'new_pattern_pattern_0': pattern,
618 'new_pattern_url_0': 'url',
606 'new_pattern_url_0': 'url',
619 'new_pattern_prefix_0': 'prefix',
607 'new_pattern_prefix_0': 'prefix',
620 'new_pattern_description_0': 'description',
608 'new_pattern_description_0': 'description',
621 'uid': old_uid,
609 'uid': old_uid,
622 'csrf_token': csrf_token
610 'csrf_token': csrf_token
623 }
611 }
624 self.app.post(post_url, post_data, status=302)
612 self.app.post(post_url, post_data, status=302)
625 settings = SettingsModel().get_all_settings()
613 settings = SettingsModel().get_all_settings()
626 assert settings[self.PATTERN_KEY+self.new_uid] == pattern
614 assert settings[self.PATTERN_KEY+self.new_uid] == pattern
627 assert self.PATTERN_KEY+old_uid not in settings
615 assert self.PATTERN_KEY+old_uid not in settings
628
616
629 @request.addfinalizer
617 @request.addfinalizer
630 def cleanup():
618 def cleanup():
631 IssueTrackerSettingsModel().delete_entries(self.new_uid)
619 IssueTrackerSettingsModel().delete_entries(self.new_uid)
632
620
633 def test_replace_issuetracker_pattern_description(
621 def test_replace_issuetracker_pattern_description(
634 self, autologin_user, csrf_token, request, settings_util):
622 self, autologin_user, csrf_token, request, settings_util):
635 prefix = 'issuetracker'
623 prefix = 'issuetracker'
636 pattern = 'issuetracker_pat'
624 pattern = 'issuetracker_pat'
637 self.uid = md5(pattern)
625 self.uid = md5(pattern)
638 pattern_key = '_'.join([prefix, 'pat', self.uid])
626 pattern_key = '_'.join([prefix, 'pat', self.uid])
639 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
627 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
640 desc_key = '_'.join([prefix, 'desc', self.uid])
628 desc_key = '_'.join([prefix, 'desc', self.uid])
641 rc_desc_key = '_'.join(['rhodecode', desc_key])
629 rc_desc_key = '_'.join(['rhodecode', desc_key])
642 new_description = 'new_description'
630 new_description = 'new_description'
643
631
644 settings_util.create_rhodecode_setting(
632 settings_util.create_rhodecode_setting(
645 pattern_key, pattern, 'unicode', cleanup=False)
633 pattern_key, pattern, 'unicode', cleanup=False)
646 settings_util.create_rhodecode_setting(
634 settings_util.create_rhodecode_setting(
647 desc_key, 'old description', 'unicode', cleanup=False)
635 desc_key, 'old description', 'unicode', cleanup=False)
648
636
649 post_url = url('admin_settings_issuetracker_save')
637 post_url = url('admin_settings_issuetracker_save')
650 post_data = {
638 post_data = {
651 'new_pattern_pattern_0': pattern,
639 'new_pattern_pattern_0': pattern,
652 'new_pattern_url_0': 'url',
640 'new_pattern_url_0': 'url',
653 'new_pattern_prefix_0': 'prefix',
641 'new_pattern_prefix_0': 'prefix',
654 'new_pattern_description_0': new_description,
642 'new_pattern_description_0': new_description,
655 'uid': self.uid,
643 'uid': self.uid,
656 'csrf_token': csrf_token
644 'csrf_token': csrf_token
657 }
645 }
658 self.app.post(post_url, post_data, status=302)
646 self.app.post(post_url, post_data, status=302)
659 settings = SettingsModel().get_all_settings()
647 settings = SettingsModel().get_all_settings()
660 assert settings[rc_pattern_key] == pattern
648 assert settings[rc_pattern_key] == pattern
661 assert settings[rc_desc_key] == new_description
649 assert settings[rc_desc_key] == new_description
662
650
663 @request.addfinalizer
651 @request.addfinalizer
664 def cleanup():
652 def cleanup():
665 IssueTrackerSettingsModel().delete_entries(self.uid)
653 IssueTrackerSettingsModel().delete_entries(self.uid)
666
654
667 def test_delete_issuetracker_pattern(
655 def test_delete_issuetracker_pattern(
668 self, autologin_user, backend, csrf_token, settings_util):
656 self, autologin_user, backend, csrf_token, settings_util):
669 pattern = 'issuetracker_pat'
657 pattern = 'issuetracker_pat'
670 uid = md5(pattern)
658 uid = md5(pattern)
671 settings_util.create_rhodecode_setting(
659 settings_util.create_rhodecode_setting(
672 self.SHORT_PATTERN_KEY+uid, pattern, 'unicode', cleanup=False)
660 self.SHORT_PATTERN_KEY+uid, pattern, 'unicode', cleanup=False)
673
661
674 post_url = url('admin_issuetracker_delete')
662 post_url = url('admin_issuetracker_delete')
675 post_data = {
663 post_data = {
676 '_method': 'delete',
664 '_method': 'delete',
677 'uid': uid,
665 'uid': uid,
678 'csrf_token': csrf_token
666 'csrf_token': csrf_token
679 }
667 }
680 self.app.post(post_url, post_data, status=302)
668 self.app.post(post_url, post_data, status=302)
681 settings = SettingsModel().get_all_settings()
669 settings = SettingsModel().get_all_settings()
682 assert 'rhodecode_%s%s' % (self.SHORT_PATTERN_KEY, uid) not in settings
670 assert 'rhodecode_%s%s' % (self.SHORT_PATTERN_KEY, uid) not in settings
@@ -1,1069 +1,1072 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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 mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.utils2 import str2bool
25 from rhodecode.model.meta import Session
25 from rhodecode.model.meta import Session
26 from rhodecode.model.settings import VcsSettingsModel, UiSetting
26 from rhodecode.model.settings import VcsSettingsModel, UiSetting
27
27
28
28
29 HOOKS_FORM_DATA = {
29 HOOKS_FORM_DATA = {
30 'hooks_changegroup_repo_size': True,
30 'hooks_changegroup_repo_size': True,
31 'hooks_changegroup_push_logger': True,
31 'hooks_changegroup_push_logger': True,
32 'hooks_outgoing_pull_logger': True
32 'hooks_outgoing_pull_logger': True
33 }
33 }
34
34
35 SVN_FORM_DATA = {
35 SVN_FORM_DATA = {
36 'new_svn_branch': 'test-branch',
36 'new_svn_branch': 'test-branch',
37 'new_svn_tag': 'test-tag'
37 'new_svn_tag': 'test-tag'
38 }
38 }
39
39
40 GENERAL_FORM_DATA = {
40 GENERAL_FORM_DATA = {
41 'rhodecode_pr_merge_enabled': True,
41 'rhodecode_pr_merge_enabled': True,
42 'rhodecode_use_outdated_comments': True,
42 'rhodecode_use_outdated_comments': True,
43 'rhodecode_hg_use_rebase_for_merging': True,
43 'rhodecode_hg_use_rebase_for_merging': True,
44 'rhodecode_hg_close_branch_before_merging': True,
45 'rhodecode_git_use_rebase_for_merging': True,
46 'rhodecode_git_close_branch_before_merging': True,
44 }
47 }
45
48
46
49
47 class TestInheritGlobalSettingsProperty(object):
50 class TestInheritGlobalSettingsProperty(object):
48 def test_get_raises_exception_when_repository_not_specified(self):
51 def test_get_raises_exception_when_repository_not_specified(self):
49 model = VcsSettingsModel()
52 model = VcsSettingsModel()
50 with pytest.raises(Exception) as exc_info:
53 with pytest.raises(Exception) as exc_info:
51 model.inherit_global_settings
54 model.inherit_global_settings
52 assert exc_info.value.message == 'Repository is not specified'
55 assert exc_info.value.message == 'Repository is not specified'
53
56
54 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
57 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
55 model = VcsSettingsModel(repo=repo_stub.repo_name)
58 model = VcsSettingsModel(repo=repo_stub.repo_name)
56 assert model.inherit_global_settings is True
59 assert model.inherit_global_settings is True
57
60
58 def test_value_is_returned(self, repo_stub, settings_util):
61 def test_value_is_returned(self, repo_stub, settings_util):
59 model = VcsSettingsModel(repo=repo_stub.repo_name)
62 model = VcsSettingsModel(repo=repo_stub.repo_name)
60 settings_util.create_repo_rhodecode_setting(
63 settings_util.create_repo_rhodecode_setting(
61 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
64 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
62 assert model.inherit_global_settings is False
65 assert model.inherit_global_settings is False
63
66
64 def test_value_is_set(self, repo_stub):
67 def test_value_is_set(self, repo_stub):
65 model = VcsSettingsModel(repo=repo_stub.repo_name)
68 model = VcsSettingsModel(repo=repo_stub.repo_name)
66 model.inherit_global_settings = False
69 model.inherit_global_settings = False
67 setting = model.repo_settings.get_setting_by_name(
70 setting = model.repo_settings.get_setting_by_name(
68 VcsSettingsModel.INHERIT_SETTINGS)
71 VcsSettingsModel.INHERIT_SETTINGS)
69 try:
72 try:
70 assert setting.app_settings_type == 'bool'
73 assert setting.app_settings_type == 'bool'
71 assert setting.app_settings_value is False
74 assert setting.app_settings_value is False
72 finally:
75 finally:
73 Session().delete(setting)
76 Session().delete(setting)
74 Session().commit()
77 Session().commit()
75
78
76 def test_set_raises_exception_when_repository_not_specified(self):
79 def test_set_raises_exception_when_repository_not_specified(self):
77 model = VcsSettingsModel()
80 model = VcsSettingsModel()
78 with pytest.raises(Exception) as exc_info:
81 with pytest.raises(Exception) as exc_info:
79 model.inherit_global_settings = False
82 model.inherit_global_settings = False
80 assert exc_info.value.message == 'Repository is not specified'
83 assert exc_info.value.message == 'Repository is not specified'
81
84
82
85
83 class TestVcsSettingsModel(object):
86 class TestVcsSettingsModel(object):
84 def test_global_svn_branch_patterns(self):
87 def test_global_svn_branch_patterns(self):
85 model = VcsSettingsModel()
88 model = VcsSettingsModel()
86 expected_result = {'test': 'test'}
89 expected_result = {'test': 'test'}
87 with mock.patch.object(model, 'global_settings') as settings_mock:
90 with mock.patch.object(model, 'global_settings') as settings_mock:
88 get_settings = settings_mock.get_ui_by_section
91 get_settings = settings_mock.get_ui_by_section
89 get_settings.return_value = expected_result
92 get_settings.return_value = expected_result
90 settings_mock.return_value = expected_result
93 settings_mock.return_value = expected_result
91 result = model.get_global_svn_branch_patterns()
94 result = model.get_global_svn_branch_patterns()
92
95
93 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
96 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
94 assert expected_result == result
97 assert expected_result == result
95
98
96 def test_repo_svn_branch_patterns(self):
99 def test_repo_svn_branch_patterns(self):
97 model = VcsSettingsModel()
100 model = VcsSettingsModel()
98 expected_result = {'test': 'test'}
101 expected_result = {'test': 'test'}
99 with mock.patch.object(model, 'repo_settings') as settings_mock:
102 with mock.patch.object(model, 'repo_settings') as settings_mock:
100 get_settings = settings_mock.get_ui_by_section
103 get_settings = settings_mock.get_ui_by_section
101 get_settings.return_value = expected_result
104 get_settings.return_value = expected_result
102 settings_mock.return_value = expected_result
105 settings_mock.return_value = expected_result
103 result = model.get_repo_svn_branch_patterns()
106 result = model.get_repo_svn_branch_patterns()
104
107
105 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
108 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
106 assert expected_result == result
109 assert expected_result == result
107
110
108 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
111 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
109 self):
112 self):
110 model = VcsSettingsModel()
113 model = VcsSettingsModel()
111 with pytest.raises(Exception) as exc_info:
114 with pytest.raises(Exception) as exc_info:
112 model.get_repo_svn_branch_patterns()
115 model.get_repo_svn_branch_patterns()
113 assert exc_info.value.message == 'Repository is not specified'
116 assert exc_info.value.message == 'Repository is not specified'
114
117
115 def test_global_svn_tag_patterns(self):
118 def test_global_svn_tag_patterns(self):
116 model = VcsSettingsModel()
119 model = VcsSettingsModel()
117 expected_result = {'test': 'test'}
120 expected_result = {'test': 'test'}
118 with mock.patch.object(model, 'global_settings') as settings_mock:
121 with mock.patch.object(model, 'global_settings') as settings_mock:
119 get_settings = settings_mock.get_ui_by_section
122 get_settings = settings_mock.get_ui_by_section
120 get_settings.return_value = expected_result
123 get_settings.return_value = expected_result
121 settings_mock.return_value = expected_result
124 settings_mock.return_value = expected_result
122 result = model.get_global_svn_tag_patterns()
125 result = model.get_global_svn_tag_patterns()
123
126
124 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
127 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
125 assert expected_result == result
128 assert expected_result == result
126
129
127 def test_repo_svn_tag_patterns(self):
130 def test_repo_svn_tag_patterns(self):
128 model = VcsSettingsModel()
131 model = VcsSettingsModel()
129 expected_result = {'test': 'test'}
132 expected_result = {'test': 'test'}
130 with mock.patch.object(model, 'repo_settings') as settings_mock:
133 with mock.patch.object(model, 'repo_settings') as settings_mock:
131 get_settings = settings_mock.get_ui_by_section
134 get_settings = settings_mock.get_ui_by_section
132 get_settings.return_value = expected_result
135 get_settings.return_value = expected_result
133 settings_mock.return_value = expected_result
136 settings_mock.return_value = expected_result
134 result = model.get_repo_svn_tag_patterns()
137 result = model.get_repo_svn_tag_patterns()
135
138
136 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
139 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
137 assert expected_result == result
140 assert expected_result == result
138
141
139 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
142 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
140 model = VcsSettingsModel()
143 model = VcsSettingsModel()
141 with pytest.raises(Exception) as exc_info:
144 with pytest.raises(Exception) as exc_info:
142 model.get_repo_svn_tag_patterns()
145 model.get_repo_svn_tag_patterns()
143 assert exc_info.value.message == 'Repository is not specified'
146 assert exc_info.value.message == 'Repository is not specified'
144
147
145 def test_get_global_settings(self):
148 def test_get_global_settings(self):
146 expected_result = {'test': 'test'}
149 expected_result = {'test': 'test'}
147 model = VcsSettingsModel()
150 model = VcsSettingsModel()
148 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
151 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
149 collect_mock.return_value = expected_result
152 collect_mock.return_value = expected_result
150 result = model.get_global_settings()
153 result = model.get_global_settings()
151
154
152 collect_mock.assert_called_once_with(global_=True)
155 collect_mock.assert_called_once_with(global_=True)
153 assert result == expected_result
156 assert result == expected_result
154
157
155 def test_get_repo_settings(self, repo_stub):
158 def test_get_repo_settings(self, repo_stub):
156 model = VcsSettingsModel(repo=repo_stub.repo_name)
159 model = VcsSettingsModel(repo=repo_stub.repo_name)
157 expected_result = {'test': 'test'}
160 expected_result = {'test': 'test'}
158 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
161 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
159 collect_mock.return_value = expected_result
162 collect_mock.return_value = expected_result
160 result = model.get_repo_settings()
163 result = model.get_repo_settings()
161
164
162 collect_mock.assert_called_once_with(global_=False)
165 collect_mock.assert_called_once_with(global_=False)
163 assert result == expected_result
166 assert result == expected_result
164
167
165 @pytest.mark.parametrize('settings, global_', [
168 @pytest.mark.parametrize('settings, global_', [
166 ('global_settings', True),
169 ('global_settings', True),
167 ('repo_settings', False)
170 ('repo_settings', False)
168 ])
171 ])
169 def test_collect_all_settings(self, settings, global_):
172 def test_collect_all_settings(self, settings, global_):
170 model = VcsSettingsModel()
173 model = VcsSettingsModel()
171 result_mock = self._mock_result()
174 result_mock = self._mock_result()
172
175
173 settings_patch = mock.patch.object(model, settings)
176 settings_patch = mock.patch.object(model, settings)
174 with settings_patch as settings_mock:
177 with settings_patch as settings_mock:
175 settings_mock.get_ui_by_section_and_key.return_value = result_mock
178 settings_mock.get_ui_by_section_and_key.return_value = result_mock
176 settings_mock.get_setting_by_name.return_value = result_mock
179 settings_mock.get_setting_by_name.return_value = result_mock
177 result = model._collect_all_settings(global_=global_)
180 result = model._collect_all_settings(global_=global_)
178
181
179 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
182 ui_settings = model.HG_SETTINGS + model.GIT_SETTINGS + model.HOOKS_SETTINGS
180 self._assert_get_settings_calls(
183 self._assert_get_settings_calls(
181 settings_mock, ui_settings, model.GENERAL_SETTINGS)
184 settings_mock, ui_settings, model.GENERAL_SETTINGS)
182 self._assert_collect_all_settings_result(
185 self._assert_collect_all_settings_result(
183 ui_settings, model.GENERAL_SETTINGS, result)
186 ui_settings, model.GENERAL_SETTINGS, result)
184
187
185 @pytest.mark.parametrize('settings, global_', [
188 @pytest.mark.parametrize('settings, global_', [
186 ('global_settings', True),
189 ('global_settings', True),
187 ('repo_settings', False)
190 ('repo_settings', False)
188 ])
191 ])
189 def test_collect_all_settings_without_empty_value(self, settings, global_):
192 def test_collect_all_settings_without_empty_value(self, settings, global_):
190 model = VcsSettingsModel()
193 model = VcsSettingsModel()
191
194
192 settings_patch = mock.patch.object(model, settings)
195 settings_patch = mock.patch.object(model, settings)
193 with settings_patch as settings_mock:
196 with settings_patch as settings_mock:
194 settings_mock.get_ui_by_section_and_key.return_value = None
197 settings_mock.get_ui_by_section_and_key.return_value = None
195 settings_mock.get_setting_by_name.return_value = None
198 settings_mock.get_setting_by_name.return_value = None
196 result = model._collect_all_settings(global_=global_)
199 result = model._collect_all_settings(global_=global_)
197
200
198 assert result == {}
201 assert result == {}
199
202
200 def _mock_result(self):
203 def _mock_result(self):
201 result_mock = mock.Mock()
204 result_mock = mock.Mock()
202 result_mock.ui_value = 'ui_value'
205 result_mock.ui_value = 'ui_value'
203 result_mock.ui_active = True
206 result_mock.ui_active = True
204 result_mock.app_settings_value = 'setting_value'
207 result_mock.app_settings_value = 'setting_value'
205 return result_mock
208 return result_mock
206
209
207 def _assert_get_settings_calls(
210 def _assert_get_settings_calls(
208 self, settings_mock, ui_settings, general_settings):
211 self, settings_mock, ui_settings, general_settings):
209 assert (
212 assert (
210 settings_mock.get_ui_by_section_and_key.call_count ==
213 settings_mock.get_ui_by_section_and_key.call_count ==
211 len(ui_settings))
214 len(ui_settings))
212 assert (
215 assert (
213 settings_mock.get_setting_by_name.call_count ==
216 settings_mock.get_setting_by_name.call_count ==
214 len(general_settings))
217 len(general_settings))
215
218
216 for section, key in ui_settings:
219 for section, key in ui_settings:
217 expected_call = mock.call(section, key)
220 expected_call = mock.call(section, key)
218 assert (
221 assert (
219 expected_call in
222 expected_call in
220 settings_mock.get_ui_by_section_and_key.call_args_list)
223 settings_mock.get_ui_by_section_and_key.call_args_list)
221
224
222 for name in general_settings:
225 for name in general_settings:
223 expected_call = mock.call(name)
226 expected_call = mock.call(name)
224 assert (
227 assert (
225 expected_call in
228 expected_call in
226 settings_mock.get_setting_by_name.call_args_list)
229 settings_mock.get_setting_by_name.call_args_list)
227
230
228 def _assert_collect_all_settings_result(
231 def _assert_collect_all_settings_result(
229 self, ui_settings, general_settings, result):
232 self, ui_settings, general_settings, result):
230 expected_result = {}
233 expected_result = {}
231 for section, key in ui_settings:
234 for section, key in ui_settings:
232 key = '{}_{}'.format(section, key.replace('.', '_'))
235 key = '{}_{}'.format(section, key.replace('.', '_'))
233
236
234 if section in ('extensions', 'hooks'):
237 if section in ('extensions', 'hooks'):
235 value = True
238 value = True
236 elif key in ['vcs_git_lfs_enabled']:
239 elif key in ['vcs_git_lfs_enabled']:
237 value = True
240 value = True
238 else:
241 else:
239 value = 'ui_value'
242 value = 'ui_value'
240 expected_result[key] = value
243 expected_result[key] = value
241
244
242 for name in general_settings:
245 for name in general_settings:
243 key = 'rhodecode_' + name
246 key = 'rhodecode_' + name
244 expected_result[key] = 'setting_value'
247 expected_result[key] = 'setting_value'
245
248
246 assert expected_result == result
249 assert expected_result == result
247
250
248
251
249 class TestCreateOrUpdateRepoHookSettings(object):
252 class TestCreateOrUpdateRepoHookSettings(object):
250 def test_create_when_no_repo_object_found(self, repo_stub):
253 def test_create_when_no_repo_object_found(self, repo_stub):
251 model = VcsSettingsModel(repo=repo_stub.repo_name)
254 model = VcsSettingsModel(repo=repo_stub.repo_name)
252
255
253 self._create_settings(model, HOOKS_FORM_DATA)
256 self._create_settings(model, HOOKS_FORM_DATA)
254
257
255 cleanup = []
258 cleanup = []
256 try:
259 try:
257 for section, key in model.HOOKS_SETTINGS:
260 for section, key in model.HOOKS_SETTINGS:
258 ui = model.repo_settings.get_ui_by_section_and_key(
261 ui = model.repo_settings.get_ui_by_section_and_key(
259 section, key)
262 section, key)
260 assert ui.ui_active is True
263 assert ui.ui_active is True
261 cleanup.append(ui)
264 cleanup.append(ui)
262 finally:
265 finally:
263 for ui in cleanup:
266 for ui in cleanup:
264 Session().delete(ui)
267 Session().delete(ui)
265 Session().commit()
268 Session().commit()
266
269
267 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
270 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
268 model = VcsSettingsModel(repo=repo_stub.repo_name)
271 model = VcsSettingsModel(repo=repo_stub.repo_name)
269
272
270 deleted_key = 'hooks_changegroup_repo_size'
273 deleted_key = 'hooks_changegroup_repo_size'
271 data = HOOKS_FORM_DATA.copy()
274 data = HOOKS_FORM_DATA.copy()
272 data.pop(deleted_key)
275 data.pop(deleted_key)
273
276
274 with pytest.raises(ValueError) as exc_info:
277 with pytest.raises(ValueError) as exc_info:
275 model.create_or_update_repo_hook_settings(data)
278 model.create_or_update_repo_hook_settings(data)
276 assert (
279 assert (
277 exc_info.value.message ==
280 exc_info.value.message ==
278 'The given data does not contain {} key'.format(deleted_key))
281 'The given data does not contain {} key'.format(deleted_key))
279
282
280 def test_update_when_repo_object_found(self, repo_stub, settings_util):
283 def test_update_when_repo_object_found(self, repo_stub, settings_util):
281 model = VcsSettingsModel(repo=repo_stub.repo_name)
284 model = VcsSettingsModel(repo=repo_stub.repo_name)
282 for section, key in model.HOOKS_SETTINGS:
285 for section, key in model.HOOKS_SETTINGS:
283 settings_util.create_repo_rhodecode_ui(
286 settings_util.create_repo_rhodecode_ui(
284 repo_stub, section, None, key=key, active=False)
287 repo_stub, section, None, key=key, active=False)
285 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
288 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
286 for section, key in model.HOOKS_SETTINGS:
289 for section, key in model.HOOKS_SETTINGS:
287 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
290 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
288 assert ui.ui_active is True
291 assert ui.ui_active is True
289
292
290 def _create_settings(self, model, data):
293 def _create_settings(self, model, data):
291 global_patch = mock.patch.object(model, 'global_settings')
294 global_patch = mock.patch.object(model, 'global_settings')
292 global_setting = mock.Mock()
295 global_setting = mock.Mock()
293 global_setting.ui_value = 'Test value'
296 global_setting.ui_value = 'Test value'
294 with global_patch as global_mock:
297 with global_patch as global_mock:
295 global_mock.get_ui_by_section_and_key.return_value = global_setting
298 global_mock.get_ui_by_section_and_key.return_value = global_setting
296 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
299 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
297
300
298
301
299 class TestUpdateGlobalHookSettings(object):
302 class TestUpdateGlobalHookSettings(object):
300 def test_update_raises_exception_when_data_incomplete(self):
303 def test_update_raises_exception_when_data_incomplete(self):
301 model = VcsSettingsModel()
304 model = VcsSettingsModel()
302
305
303 deleted_key = 'hooks_changegroup_repo_size'
306 deleted_key = 'hooks_changegroup_repo_size'
304 data = HOOKS_FORM_DATA.copy()
307 data = HOOKS_FORM_DATA.copy()
305 data.pop(deleted_key)
308 data.pop(deleted_key)
306
309
307 with pytest.raises(ValueError) as exc_info:
310 with pytest.raises(ValueError) as exc_info:
308 model.update_global_hook_settings(data)
311 model.update_global_hook_settings(data)
309 assert (
312 assert (
310 exc_info.value.message ==
313 exc_info.value.message ==
311 'The given data does not contain {} key'.format(deleted_key))
314 'The given data does not contain {} key'.format(deleted_key))
312
315
313 def test_update_global_hook_settings(self, settings_util):
316 def test_update_global_hook_settings(self, settings_util):
314 model = VcsSettingsModel()
317 model = VcsSettingsModel()
315 setting_mock = mock.MagicMock()
318 setting_mock = mock.MagicMock()
316 setting_mock.ui_active = False
319 setting_mock.ui_active = False
317 get_settings_patcher = mock.patch.object(
320 get_settings_patcher = mock.patch.object(
318 model.global_settings, 'get_ui_by_section_and_key',
321 model.global_settings, 'get_ui_by_section_and_key',
319 return_value=setting_mock)
322 return_value=setting_mock)
320 session_patcher = mock.patch('rhodecode.model.settings.Session')
323 session_patcher = mock.patch('rhodecode.model.settings.Session')
321 with get_settings_patcher as get_settings_mock, session_patcher:
324 with get_settings_patcher as get_settings_mock, session_patcher:
322 model.update_global_hook_settings(HOOKS_FORM_DATA)
325 model.update_global_hook_settings(HOOKS_FORM_DATA)
323 assert setting_mock.ui_active is True
326 assert setting_mock.ui_active is True
324 assert get_settings_mock.call_count == 3
327 assert get_settings_mock.call_count == 3
325
328
326
329
327 class TestCreateOrUpdateRepoGeneralSettings(object):
330 class TestCreateOrUpdateRepoGeneralSettings(object):
328 def test_calls_create_or_update_general_settings(self, repo_stub):
331 def test_calls_create_or_update_general_settings(self, repo_stub):
329 model = VcsSettingsModel(repo=repo_stub.repo_name)
332 model = VcsSettingsModel(repo=repo_stub.repo_name)
330 create_patch = mock.patch.object(
333 create_patch = mock.patch.object(
331 model, '_create_or_update_general_settings')
334 model, '_create_or_update_general_settings')
332 with create_patch as create_mock:
335 with create_patch as create_mock:
333 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
336 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
334 create_mock.assert_called_once_with(
337 create_mock.assert_called_once_with(
335 model.repo_settings, GENERAL_FORM_DATA)
338 model.repo_settings, GENERAL_FORM_DATA)
336
339
337 def test_raises_exception_when_repository_is_not_specified(self):
340 def test_raises_exception_when_repository_is_not_specified(self):
338 model = VcsSettingsModel()
341 model = VcsSettingsModel()
339 with pytest.raises(Exception) as exc_info:
342 with pytest.raises(Exception) as exc_info:
340 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
343 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
341 assert exc_info.value.message == 'Repository is not specified'
344 assert exc_info.value.message == 'Repository is not specified'
342
345
343
346
344 class TestCreateOrUpdatGlobalGeneralSettings(object):
347 class TestCreateOrUpdatGlobalGeneralSettings(object):
345 def test_calls_create_or_update_general_settings(self):
348 def test_calls_create_or_update_general_settings(self):
346 model = VcsSettingsModel()
349 model = VcsSettingsModel()
347 create_patch = mock.patch.object(
350 create_patch = mock.patch.object(
348 model, '_create_or_update_general_settings')
351 model, '_create_or_update_general_settings')
349 with create_patch as create_mock:
352 with create_patch as create_mock:
350 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
353 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
351 create_mock.assert_called_once_with(
354 create_mock.assert_called_once_with(
352 model.global_settings, GENERAL_FORM_DATA)
355 model.global_settings, GENERAL_FORM_DATA)
353
356
354
357
355 class TestCreateOrUpdateGeneralSettings(object):
358 class TestCreateOrUpdateGeneralSettings(object):
356 def test_create_when_no_repo_settings_found(self, repo_stub):
359 def test_create_when_no_repo_settings_found(self, repo_stub):
357 model = VcsSettingsModel(repo=repo_stub.repo_name)
360 model = VcsSettingsModel(repo=repo_stub.repo_name)
358 model._create_or_update_general_settings(
361 model._create_or_update_general_settings(
359 model.repo_settings, GENERAL_FORM_DATA)
362 model.repo_settings, GENERAL_FORM_DATA)
360
363
361 cleanup = []
364 cleanup = []
362 try:
365 try:
363 for name in model.GENERAL_SETTINGS:
366 for name in model.GENERAL_SETTINGS:
364 setting = model.repo_settings.get_setting_by_name(name)
367 setting = model.repo_settings.get_setting_by_name(name)
365 assert setting.app_settings_value is True
368 assert setting.app_settings_value is True
366 cleanup.append(setting)
369 cleanup.append(setting)
367 finally:
370 finally:
368 for setting in cleanup:
371 for setting in cleanup:
369 Session().delete(setting)
372 Session().delete(setting)
370 Session().commit()
373 Session().commit()
371
374
372 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
375 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
373 model = VcsSettingsModel(repo=repo_stub.repo_name)
376 model = VcsSettingsModel(repo=repo_stub.repo_name)
374
377
375 deleted_key = 'rhodecode_pr_merge_enabled'
378 deleted_key = 'rhodecode_pr_merge_enabled'
376 data = GENERAL_FORM_DATA.copy()
379 data = GENERAL_FORM_DATA.copy()
377 data.pop(deleted_key)
380 data.pop(deleted_key)
378
381
379 with pytest.raises(ValueError) as exc_info:
382 with pytest.raises(ValueError) as exc_info:
380 model._create_or_update_general_settings(model.repo_settings, data)
383 model._create_or_update_general_settings(model.repo_settings, data)
381 assert (
384 assert (
382 exc_info.value.message ==
385 exc_info.value.message ==
383 'The given data does not contain {} key'.format(deleted_key))
386 'The given data does not contain {} key'.format(deleted_key))
384
387
385 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
388 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
386 model = VcsSettingsModel(repo=repo_stub.repo_name)
389 model = VcsSettingsModel(repo=repo_stub.repo_name)
387 for name in model.GENERAL_SETTINGS:
390 for name in model.GENERAL_SETTINGS:
388 settings_util.create_repo_rhodecode_setting(
391 settings_util.create_repo_rhodecode_setting(
389 repo_stub, name, False, 'bool')
392 repo_stub, name, False, 'bool')
390
393
391 model._create_or_update_general_settings(
394 model._create_or_update_general_settings(
392 model.repo_settings, GENERAL_FORM_DATA)
395 model.repo_settings, GENERAL_FORM_DATA)
393
396
394 for name in model.GENERAL_SETTINGS:
397 for name in model.GENERAL_SETTINGS:
395 setting = model.repo_settings.get_setting_by_name(name)
398 setting = model.repo_settings.get_setting_by_name(name)
396 assert setting.app_settings_value is True
399 assert setting.app_settings_value is True
397
400
398
401
399 class TestCreateRepoSvnSettings(object):
402 class TestCreateRepoSvnSettings(object):
400 def test_calls_create_svn_settings(self, repo_stub):
403 def test_calls_create_svn_settings(self, repo_stub):
401 model = VcsSettingsModel(repo=repo_stub.repo_name)
404 model = VcsSettingsModel(repo=repo_stub.repo_name)
402 with mock.patch.object(model, '_create_svn_settings') as create_mock:
405 with mock.patch.object(model, '_create_svn_settings') as create_mock:
403 model.create_repo_svn_settings(SVN_FORM_DATA)
406 model.create_repo_svn_settings(SVN_FORM_DATA)
404 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
407 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
405
408
406 def test_raises_exception_when_repository_is_not_specified(self):
409 def test_raises_exception_when_repository_is_not_specified(self):
407 model = VcsSettingsModel()
410 model = VcsSettingsModel()
408 with pytest.raises(Exception) as exc_info:
411 with pytest.raises(Exception) as exc_info:
409 model.create_repo_svn_settings(SVN_FORM_DATA)
412 model.create_repo_svn_settings(SVN_FORM_DATA)
410 assert exc_info.value.message == 'Repository is not specified'
413 assert exc_info.value.message == 'Repository is not specified'
411
414
412
415
413 class TestCreateSvnSettings(object):
416 class TestCreateSvnSettings(object):
414 def test_create(self, repo_stub):
417 def test_create(self, repo_stub):
415 model = VcsSettingsModel(repo=repo_stub.repo_name)
418 model = VcsSettingsModel(repo=repo_stub.repo_name)
416 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
419 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
417 Session().commit()
420 Session().commit()
418
421
419 branch_ui = model.repo_settings.get_ui_by_section(
422 branch_ui = model.repo_settings.get_ui_by_section(
420 model.SVN_BRANCH_SECTION)
423 model.SVN_BRANCH_SECTION)
421 tag_ui = model.repo_settings.get_ui_by_section(
424 tag_ui = model.repo_settings.get_ui_by_section(
422 model.SVN_TAG_SECTION)
425 model.SVN_TAG_SECTION)
423
426
424 try:
427 try:
425 assert len(branch_ui) == 1
428 assert len(branch_ui) == 1
426 assert len(tag_ui) == 1
429 assert len(tag_ui) == 1
427 finally:
430 finally:
428 Session().delete(branch_ui[0])
431 Session().delete(branch_ui[0])
429 Session().delete(tag_ui[0])
432 Session().delete(tag_ui[0])
430 Session().commit()
433 Session().commit()
431
434
432 def test_create_tag(self, repo_stub):
435 def test_create_tag(self, repo_stub):
433 model = VcsSettingsModel(repo=repo_stub.repo_name)
436 model = VcsSettingsModel(repo=repo_stub.repo_name)
434 data = SVN_FORM_DATA.copy()
437 data = SVN_FORM_DATA.copy()
435 data.pop('new_svn_branch')
438 data.pop('new_svn_branch')
436 model._create_svn_settings(model.repo_settings, data)
439 model._create_svn_settings(model.repo_settings, data)
437 Session().commit()
440 Session().commit()
438
441
439 branch_ui = model.repo_settings.get_ui_by_section(
442 branch_ui = model.repo_settings.get_ui_by_section(
440 model.SVN_BRANCH_SECTION)
443 model.SVN_BRANCH_SECTION)
441 tag_ui = model.repo_settings.get_ui_by_section(
444 tag_ui = model.repo_settings.get_ui_by_section(
442 model.SVN_TAG_SECTION)
445 model.SVN_TAG_SECTION)
443
446
444 try:
447 try:
445 assert len(branch_ui) == 0
448 assert len(branch_ui) == 0
446 assert len(tag_ui) == 1
449 assert len(tag_ui) == 1
447 finally:
450 finally:
448 Session().delete(tag_ui[0])
451 Session().delete(tag_ui[0])
449 Session().commit()
452 Session().commit()
450
453
451 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
454 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
452 model = VcsSettingsModel(repo=repo_stub.repo_name)
455 model = VcsSettingsModel(repo=repo_stub.repo_name)
453 model._create_svn_settings(model.repo_settings, {})
456 model._create_svn_settings(model.repo_settings, {})
454 Session().commit()
457 Session().commit()
455
458
456 branch_ui = model.repo_settings.get_ui_by_section(
459 branch_ui = model.repo_settings.get_ui_by_section(
457 model.SVN_BRANCH_SECTION)
460 model.SVN_BRANCH_SECTION)
458 tag_ui = model.repo_settings.get_ui_by_section(
461 tag_ui = model.repo_settings.get_ui_by_section(
459 model.SVN_TAG_SECTION)
462 model.SVN_TAG_SECTION)
460
463
461 assert len(branch_ui) == 0
464 assert len(branch_ui) == 0
462 assert len(tag_ui) == 0
465 assert len(tag_ui) == 0
463
466
464 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
467 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
465 model = VcsSettingsModel(repo=repo_stub.repo_name)
468 model = VcsSettingsModel(repo=repo_stub.repo_name)
466 data = {
469 data = {
467 'new_svn_branch': '',
470 'new_svn_branch': '',
468 'new_svn_tag': ''
471 'new_svn_tag': ''
469 }
472 }
470 model._create_svn_settings(model.repo_settings, data)
473 model._create_svn_settings(model.repo_settings, data)
471 Session().commit()
474 Session().commit()
472
475
473 branch_ui = model.repo_settings.get_ui_by_section(
476 branch_ui = model.repo_settings.get_ui_by_section(
474 model.SVN_BRANCH_SECTION)
477 model.SVN_BRANCH_SECTION)
475 tag_ui = model.repo_settings.get_ui_by_section(
478 tag_ui = model.repo_settings.get_ui_by_section(
476 model.SVN_TAG_SECTION)
479 model.SVN_TAG_SECTION)
477
480
478 assert len(branch_ui) == 0
481 assert len(branch_ui) == 0
479 assert len(tag_ui) == 0
482 assert len(tag_ui) == 0
480
483
481
484
482 class TestCreateOrUpdateUi(object):
485 class TestCreateOrUpdateUi(object):
483 def test_create(self, repo_stub):
486 def test_create(self, repo_stub):
484 model = VcsSettingsModel(repo=repo_stub.repo_name)
487 model = VcsSettingsModel(repo=repo_stub.repo_name)
485 model._create_or_update_ui(
488 model._create_or_update_ui(
486 model.repo_settings, 'test-section', 'test-key', active=False,
489 model.repo_settings, 'test-section', 'test-key', active=False,
487 value='False')
490 value='False')
488 Session().commit()
491 Session().commit()
489
492
490 created_ui = model.repo_settings.get_ui_by_section_and_key(
493 created_ui = model.repo_settings.get_ui_by_section_and_key(
491 'test-section', 'test-key')
494 'test-section', 'test-key')
492
495
493 try:
496 try:
494 assert created_ui.ui_active is False
497 assert created_ui.ui_active is False
495 assert str2bool(created_ui.ui_value) is False
498 assert str2bool(created_ui.ui_value) is False
496 finally:
499 finally:
497 Session().delete(created_ui)
500 Session().delete(created_ui)
498 Session().commit()
501 Session().commit()
499
502
500 def test_update(self, repo_stub, settings_util):
503 def test_update(self, repo_stub, settings_util):
501 model = VcsSettingsModel(repo=repo_stub.repo_name)
504 model = VcsSettingsModel(repo=repo_stub.repo_name)
502
505
503 largefiles, phases, evolve = model.HG_SETTINGS
506 largefiles, phases, evolve = model.HG_SETTINGS
504
507
505 section = 'test-section'
508 section = 'test-section'
506 key = 'test-key'
509 key = 'test-key'
507 settings_util.create_repo_rhodecode_ui(
510 settings_util.create_repo_rhodecode_ui(
508 repo_stub, section, 'True', key=key, active=True)
511 repo_stub, section, 'True', key=key, active=True)
509
512
510 model._create_or_update_ui(
513 model._create_or_update_ui(
511 model.repo_settings, section, key, active=False, value='False')
514 model.repo_settings, section, key, active=False, value='False')
512 Session().commit()
515 Session().commit()
513
516
514 created_ui = model.repo_settings.get_ui_by_section_and_key(
517 created_ui = model.repo_settings.get_ui_by_section_and_key(
515 section, key)
518 section, key)
516 assert created_ui.ui_active is False
519 assert created_ui.ui_active is False
517 assert str2bool(created_ui.ui_value) is False
520 assert str2bool(created_ui.ui_value) is False
518
521
519
522
520 class TestCreateOrUpdateRepoHgSettings(object):
523 class TestCreateOrUpdateRepoHgSettings(object):
521 FORM_DATA = {
524 FORM_DATA = {
522 'extensions_largefiles': False,
525 'extensions_largefiles': False,
523 'extensions_evolve': False,
526 'extensions_evolve': False,
524 'phases_publish': False
527 'phases_publish': False
525 }
528 }
526
529
527 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
530 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
528 model = VcsSettingsModel(repo=repo_stub.repo_name)
531 model = VcsSettingsModel(repo=repo_stub.repo_name)
529 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
532 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
530 model.create_or_update_repo_hg_settings(self.FORM_DATA)
533 model.create_or_update_repo_hg_settings(self.FORM_DATA)
531 expected_calls = [
534 expected_calls = [
532 mock.call(model.repo_settings, 'extensions', 'largefiles',
535 mock.call(model.repo_settings, 'extensions', 'largefiles',
533 active=False, value=''),
536 active=False, value=''),
534 mock.call(model.repo_settings, 'extensions', 'evolve',
537 mock.call(model.repo_settings, 'extensions', 'evolve',
535 active=False, value=''),
538 active=False, value=''),
536 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
539 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
537 ]
540 ]
538 assert expected_calls == create_mock.call_args_list
541 assert expected_calls == create_mock.call_args_list
539
542
540 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
543 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
541 def test_key_is_not_found(self, repo_stub, field_to_remove):
544 def test_key_is_not_found(self, repo_stub, field_to_remove):
542 model = VcsSettingsModel(repo=repo_stub.repo_name)
545 model = VcsSettingsModel(repo=repo_stub.repo_name)
543 data = self.FORM_DATA.copy()
546 data = self.FORM_DATA.copy()
544 data.pop(field_to_remove)
547 data.pop(field_to_remove)
545 with pytest.raises(ValueError) as exc_info:
548 with pytest.raises(ValueError) as exc_info:
546 model.create_or_update_repo_hg_settings(data)
549 model.create_or_update_repo_hg_settings(data)
547 expected_message = 'The given data does not contain {} key'.format(
550 expected_message = 'The given data does not contain {} key'.format(
548 field_to_remove)
551 field_to_remove)
549 assert exc_info.value.message == expected_message
552 assert exc_info.value.message == expected_message
550
553
551 def test_create_raises_exception_when_repository_not_specified(self):
554 def test_create_raises_exception_when_repository_not_specified(self):
552 model = VcsSettingsModel()
555 model = VcsSettingsModel()
553 with pytest.raises(Exception) as exc_info:
556 with pytest.raises(Exception) as exc_info:
554 model.create_or_update_repo_hg_settings(self.FORM_DATA)
557 model.create_or_update_repo_hg_settings(self.FORM_DATA)
555 assert exc_info.value.message == 'Repository is not specified'
558 assert exc_info.value.message == 'Repository is not specified'
556
559
557
560
558 class TestUpdateGlobalSslSetting(object):
561 class TestUpdateGlobalSslSetting(object):
559 def test_updates_global_hg_settings(self):
562 def test_updates_global_hg_settings(self):
560 model = VcsSettingsModel()
563 model = VcsSettingsModel()
561 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
564 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
562 model.update_global_ssl_setting('False')
565 model.update_global_ssl_setting('False')
563 create_mock.assert_called_once_with(
566 create_mock.assert_called_once_with(
564 model.global_settings, 'web', 'push_ssl', value='False')
567 model.global_settings, 'web', 'push_ssl', value='False')
565
568
566
569
567 class TestUpdateGlobalPathSetting(object):
570 class TestUpdateGlobalPathSetting(object):
568 def test_updates_global_path_settings(self):
571 def test_updates_global_path_settings(self):
569 model = VcsSettingsModel()
572 model = VcsSettingsModel()
570 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
573 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
571 model.update_global_path_setting('False')
574 model.update_global_path_setting('False')
572 create_mock.assert_called_once_with(
575 create_mock.assert_called_once_with(
573 model.global_settings, 'paths', '/', value='False')
576 model.global_settings, 'paths', '/', value='False')
574
577
575
578
576 class TestCreateOrUpdateGlobalHgSettings(object):
579 class TestCreateOrUpdateGlobalHgSettings(object):
577 FORM_DATA = {
580 FORM_DATA = {
578 'extensions_largefiles': False,
581 'extensions_largefiles': False,
579 'largefiles_usercache': '/example/largefiles-store',
582 'largefiles_usercache': '/example/largefiles-store',
580 'phases_publish': False,
583 'phases_publish': False,
581 'extensions_hgsubversion': False,
584 'extensions_hgsubversion': False,
582 'extensions_evolve': False
585 'extensions_evolve': False
583 }
586 }
584
587
585 def test_creates_repo_hg_settings_when_data_is_correct(self):
588 def test_creates_repo_hg_settings_when_data_is_correct(self):
586 model = VcsSettingsModel()
589 model = VcsSettingsModel()
587 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
590 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
588 model.create_or_update_global_hg_settings(self.FORM_DATA)
591 model.create_or_update_global_hg_settings(self.FORM_DATA)
589 expected_calls = [
592 expected_calls = [
590 mock.call(model.global_settings, 'extensions', 'largefiles',
593 mock.call(model.global_settings, 'extensions', 'largefiles',
591 active=False, value=''),
594 active=False, value=''),
592 mock.call(model.global_settings, 'largefiles', 'usercache',
595 mock.call(model.global_settings, 'largefiles', 'usercache',
593 value='/example/largefiles-store'),
596 value='/example/largefiles-store'),
594 mock.call(model.global_settings, 'phases', 'publish',
597 mock.call(model.global_settings, 'phases', 'publish',
595 value='False'),
598 value='False'),
596 mock.call(model.global_settings, 'extensions', 'hgsubversion',
599 mock.call(model.global_settings, 'extensions', 'hgsubversion',
597 active=False),
600 active=False),
598 mock.call(model.global_settings, 'extensions', 'evolve',
601 mock.call(model.global_settings, 'extensions', 'evolve',
599 active=False, value='')
602 active=False, value='')
600 ]
603 ]
601 assert expected_calls == create_mock.call_args_list
604 assert expected_calls == create_mock.call_args_list
602
605
603 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
606 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
604 def test_key_is_not_found(self, repo_stub, field_to_remove):
607 def test_key_is_not_found(self, repo_stub, field_to_remove):
605 model = VcsSettingsModel(repo=repo_stub.repo_name)
608 model = VcsSettingsModel(repo=repo_stub.repo_name)
606 data = self.FORM_DATA.copy()
609 data = self.FORM_DATA.copy()
607 data.pop(field_to_remove)
610 data.pop(field_to_remove)
608 with pytest.raises(Exception) as exc_info:
611 with pytest.raises(Exception) as exc_info:
609 model.create_or_update_global_hg_settings(data)
612 model.create_or_update_global_hg_settings(data)
610 expected_message = 'The given data does not contain {} key'.format(
613 expected_message = 'The given data does not contain {} key'.format(
611 field_to_remove)
614 field_to_remove)
612 assert exc_info.value.message == expected_message
615 assert exc_info.value.message == expected_message
613
616
614
617
615 class TestCreateOrUpdateGlobalGitSettings(object):
618 class TestCreateOrUpdateGlobalGitSettings(object):
616 FORM_DATA = {
619 FORM_DATA = {
617 'vcs_git_lfs_enabled': False,
620 'vcs_git_lfs_enabled': False,
618 'vcs_git_lfs_store_location': '/example/lfs-store',
621 'vcs_git_lfs_store_location': '/example/lfs-store',
619 }
622 }
620
623
621 def test_creates_repo_hg_settings_when_data_is_correct(self):
624 def test_creates_repo_hg_settings_when_data_is_correct(self):
622 model = VcsSettingsModel()
625 model = VcsSettingsModel()
623 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
626 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
624 model.create_or_update_global_git_settings(self.FORM_DATA)
627 model.create_or_update_global_git_settings(self.FORM_DATA)
625 expected_calls = [
628 expected_calls = [
626 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled',
629 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled',
627 active=False, value=False),
630 active=False, value=False),
628 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location',
631 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location',
629 value='/example/lfs-store'),
632 value='/example/lfs-store'),
630 ]
633 ]
631 assert expected_calls == create_mock.call_args_list
634 assert expected_calls == create_mock.call_args_list
632
635
633
636
634 class TestDeleteRepoSvnPattern(object):
637 class TestDeleteRepoSvnPattern(object):
635 def test_success_when_repo_is_set(self, backend_svn):
638 def test_success_when_repo_is_set(self, backend_svn):
636 repo_name = backend_svn.repo_name
639 repo_name = backend_svn.repo_name
637 model = VcsSettingsModel(repo=repo_name)
640 model = VcsSettingsModel(repo=repo_name)
638 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
641 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
639 with delete_ui_patch as delete_ui_mock:
642 with delete_ui_patch as delete_ui_mock:
640 model.delete_repo_svn_pattern(123)
643 model.delete_repo_svn_pattern(123)
641 delete_ui_mock.assert_called_once_with(123)
644 delete_ui_mock.assert_called_once_with(123)
642
645
643 def test_raises_exception_when_repository_is_not_specified(self):
646 def test_raises_exception_when_repository_is_not_specified(self):
644 model = VcsSettingsModel()
647 model = VcsSettingsModel()
645 with pytest.raises(Exception) as exc_info:
648 with pytest.raises(Exception) as exc_info:
646 model.delete_repo_svn_pattern(123)
649 model.delete_repo_svn_pattern(123)
647 assert exc_info.value.message == 'Repository is not specified'
650 assert exc_info.value.message == 'Repository is not specified'
648
651
649
652
650 class TestDeleteGlobalSvnPattern(object):
653 class TestDeleteGlobalSvnPattern(object):
651 def test_delete_global_svn_pattern_calls_delete_ui(self):
654 def test_delete_global_svn_pattern_calls_delete_ui(self):
652 model = VcsSettingsModel()
655 model = VcsSettingsModel()
653 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
656 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
654 with delete_ui_patch as delete_ui_mock:
657 with delete_ui_patch as delete_ui_mock:
655 model.delete_global_svn_pattern(123)
658 model.delete_global_svn_pattern(123)
656 delete_ui_mock.assert_called_once_with(123)
659 delete_ui_mock.assert_called_once_with(123)
657
660
658
661
659 class TestFilterUiSettings(object):
662 class TestFilterUiSettings(object):
660 def test_settings_are_filtered(self):
663 def test_settings_are_filtered(self):
661 model = VcsSettingsModel()
664 model = VcsSettingsModel()
662 repo_settings = [
665 repo_settings = [
663 UiSetting('extensions', 'largefiles', '', True),
666 UiSetting('extensions', 'largefiles', '', True),
664 UiSetting('phases', 'publish', 'True', True),
667 UiSetting('phases', 'publish', 'True', True),
665 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
668 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
666 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
669 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
667 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
670 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
668 UiSetting(
671 UiSetting(
669 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
672 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
670 'test_branch', True),
673 'test_branch', True),
671 UiSetting(
674 UiSetting(
672 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
675 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
673 'test_tag', True),
676 'test_tag', True),
674 ]
677 ]
675 non_repo_settings = [
678 non_repo_settings = [
676 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
679 UiSetting('largefiles', 'usercache', '/example/largefiles-store', True),
677 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
680 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
678 UiSetting('hooks', 'test2', 'hook', True),
681 UiSetting('hooks', 'test2', 'hook', True),
679 UiSetting(
682 UiSetting(
680 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
683 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
681 'test_tag', True),
684 'test_tag', True),
682 ]
685 ]
683 settings = repo_settings + non_repo_settings
686 settings = repo_settings + non_repo_settings
684 filtered_settings = model._filter_ui_settings(settings)
687 filtered_settings = model._filter_ui_settings(settings)
685 assert sorted(filtered_settings) == sorted(repo_settings)
688 assert sorted(filtered_settings) == sorted(repo_settings)
686
689
687
690
688 class TestFilterGeneralSettings(object):
691 class TestFilterGeneralSettings(object):
689 def test_settings_are_filtered(self):
692 def test_settings_are_filtered(self):
690 model = VcsSettingsModel()
693 model = VcsSettingsModel()
691 settings = {
694 settings = {
692 'rhodecode_abcde': 'value1',
695 'rhodecode_abcde': 'value1',
693 'rhodecode_vwxyz': 'value2',
696 'rhodecode_vwxyz': 'value2',
694 }
697 }
695 general_settings = {
698 general_settings = {
696 'rhodecode_{}'.format(key): 'value'
699 'rhodecode_{}'.format(key): 'value'
697 for key in VcsSettingsModel.GENERAL_SETTINGS
700 for key in VcsSettingsModel.GENERAL_SETTINGS
698 }
701 }
699 settings.update(general_settings)
702 settings.update(general_settings)
700
703
701 filtered_settings = model._filter_general_settings(general_settings)
704 filtered_settings = model._filter_general_settings(general_settings)
702 assert sorted(filtered_settings) == sorted(general_settings)
705 assert sorted(filtered_settings) == sorted(general_settings)
703
706
704
707
705 class TestGetRepoUiSettings(object):
708 class TestGetRepoUiSettings(object):
706 def test_global_uis_are_returned_when_no_repo_uis_found(
709 def test_global_uis_are_returned_when_no_repo_uis_found(
707 self, repo_stub):
710 self, repo_stub):
708 model = VcsSettingsModel(repo=repo_stub.repo_name)
711 model = VcsSettingsModel(repo=repo_stub.repo_name)
709 result = model.get_repo_ui_settings()
712 result = model.get_repo_ui_settings()
710 svn_sections = (
713 svn_sections = (
711 VcsSettingsModel.SVN_TAG_SECTION,
714 VcsSettingsModel.SVN_TAG_SECTION,
712 VcsSettingsModel.SVN_BRANCH_SECTION)
715 VcsSettingsModel.SVN_BRANCH_SECTION)
713 expected_result = [
716 expected_result = [
714 s for s in model.global_settings.get_ui()
717 s for s in model.global_settings.get_ui()
715 if s.section not in svn_sections]
718 if s.section not in svn_sections]
716 assert sorted(result) == sorted(expected_result)
719 assert sorted(result) == sorted(expected_result)
717
720
718 def test_repo_uis_are_overriding_global_uis(
721 def test_repo_uis_are_overriding_global_uis(
719 self, repo_stub, settings_util):
722 self, repo_stub, settings_util):
720 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
723 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
721 settings_util.create_repo_rhodecode_ui(
724 settings_util.create_repo_rhodecode_ui(
722 repo_stub, section, 'repo', key=key, active=False)
725 repo_stub, section, 'repo', key=key, active=False)
723 model = VcsSettingsModel(repo=repo_stub.repo_name)
726 model = VcsSettingsModel(repo=repo_stub.repo_name)
724 result = model.get_repo_ui_settings()
727 result = model.get_repo_ui_settings()
725 for setting in result:
728 for setting in result:
726 locator = (setting.section, setting.key)
729 locator = (setting.section, setting.key)
727 if locator in VcsSettingsModel.HOOKS_SETTINGS:
730 if locator in VcsSettingsModel.HOOKS_SETTINGS:
728 assert setting.value == 'repo'
731 assert setting.value == 'repo'
729
732
730 assert setting.active is False
733 assert setting.active is False
731
734
732 def test_global_svn_patterns_are_not_in_list(
735 def test_global_svn_patterns_are_not_in_list(
733 self, repo_stub, settings_util):
736 self, repo_stub, settings_util):
734 svn_sections = (
737 svn_sections = (
735 VcsSettingsModel.SVN_TAG_SECTION,
738 VcsSettingsModel.SVN_TAG_SECTION,
736 VcsSettingsModel.SVN_BRANCH_SECTION)
739 VcsSettingsModel.SVN_BRANCH_SECTION)
737 for section in svn_sections:
740 for section in svn_sections:
738 settings_util.create_rhodecode_ui(
741 settings_util.create_rhodecode_ui(
739 section, 'repo', key='deadbeef' + section, active=False)
742 section, 'repo', key='deadbeef' + section, active=False)
740 model = VcsSettingsModel(repo=repo_stub.repo_name)
743 model = VcsSettingsModel(repo=repo_stub.repo_name)
741 result = model.get_repo_ui_settings()
744 result = model.get_repo_ui_settings()
742 for setting in result:
745 for setting in result:
743 assert setting.section not in svn_sections
746 assert setting.section not in svn_sections
744
747
745 def test_repo_uis_filtered_by_section_are_returned(
748 def test_repo_uis_filtered_by_section_are_returned(
746 self, repo_stub, settings_util):
749 self, repo_stub, settings_util):
747 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
750 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
748 settings_util.create_repo_rhodecode_ui(
751 settings_util.create_repo_rhodecode_ui(
749 repo_stub, section, 'repo', key=key, active=False)
752 repo_stub, section, 'repo', key=key, active=False)
750 model = VcsSettingsModel(repo=repo_stub.repo_name)
753 model = VcsSettingsModel(repo=repo_stub.repo_name)
751 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
754 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
752 result = model.get_repo_ui_settings(section=section)
755 result = model.get_repo_ui_settings(section=section)
753 for setting in result:
756 for setting in result:
754 assert setting.section == section
757 assert setting.section == section
755
758
756 def test_repo_uis_filtered_by_key_are_returned(
759 def test_repo_uis_filtered_by_key_are_returned(
757 self, repo_stub, settings_util):
760 self, repo_stub, settings_util):
758 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
761 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
759 settings_util.create_repo_rhodecode_ui(
762 settings_util.create_repo_rhodecode_ui(
760 repo_stub, section, 'repo', key=key, active=False)
763 repo_stub, section, 'repo', key=key, active=False)
761 model = VcsSettingsModel(repo=repo_stub.repo_name)
764 model = VcsSettingsModel(repo=repo_stub.repo_name)
762 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
765 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
763 result = model.get_repo_ui_settings(key=key)
766 result = model.get_repo_ui_settings(key=key)
764 for setting in result:
767 for setting in result:
765 assert setting.key == key
768 assert setting.key == key
766
769
767 def test_raises_exception_when_repository_is_not_specified(self):
770 def test_raises_exception_when_repository_is_not_specified(self):
768 model = VcsSettingsModel()
771 model = VcsSettingsModel()
769 with pytest.raises(Exception) as exc_info:
772 with pytest.raises(Exception) as exc_info:
770 model.get_repo_ui_settings()
773 model.get_repo_ui_settings()
771 assert exc_info.value.message == 'Repository is not specified'
774 assert exc_info.value.message == 'Repository is not specified'
772
775
773
776
774 class TestGetRepoGeneralSettings(object):
777 class TestGetRepoGeneralSettings(object):
775 def test_global_settings_are_returned_when_no_repo_settings_found(
778 def test_global_settings_are_returned_when_no_repo_settings_found(
776 self, repo_stub):
779 self, repo_stub):
777 model = VcsSettingsModel(repo=repo_stub.repo_name)
780 model = VcsSettingsModel(repo=repo_stub.repo_name)
778 result = model.get_repo_general_settings()
781 result = model.get_repo_general_settings()
779 expected_result = model.global_settings.get_all_settings()
782 expected_result = model.global_settings.get_all_settings()
780 assert sorted(result) == sorted(expected_result)
783 assert sorted(result) == sorted(expected_result)
781
784
782 def test_repo_uis_are_overriding_global_uis(
785 def test_repo_uis_are_overriding_global_uis(
783 self, repo_stub, settings_util):
786 self, repo_stub, settings_util):
784 for key in VcsSettingsModel.GENERAL_SETTINGS:
787 for key in VcsSettingsModel.GENERAL_SETTINGS:
785 settings_util.create_repo_rhodecode_setting(
788 settings_util.create_repo_rhodecode_setting(
786 repo_stub, key, 'abcde', type_='unicode')
789 repo_stub, key, 'abcde', type_='unicode')
787 model = VcsSettingsModel(repo=repo_stub.repo_name)
790 model = VcsSettingsModel(repo=repo_stub.repo_name)
788 result = model.get_repo_ui_settings()
791 result = model.get_repo_ui_settings()
789 for key in result:
792 for key in result:
790 if key in VcsSettingsModel.GENERAL_SETTINGS:
793 if key in VcsSettingsModel.GENERAL_SETTINGS:
791 assert result[key] == 'abcde'
794 assert result[key] == 'abcde'
792
795
793 def test_raises_exception_when_repository_is_not_specified(self):
796 def test_raises_exception_when_repository_is_not_specified(self):
794 model = VcsSettingsModel()
797 model = VcsSettingsModel()
795 with pytest.raises(Exception) as exc_info:
798 with pytest.raises(Exception) as exc_info:
796 model.get_repo_general_settings()
799 model.get_repo_general_settings()
797 assert exc_info.value.message == 'Repository is not specified'
800 assert exc_info.value.message == 'Repository is not specified'
798
801
799
802
800 class TestGetGlobalGeneralSettings(object):
803 class TestGetGlobalGeneralSettings(object):
801 def test_global_settings_are_returned(self, repo_stub):
804 def test_global_settings_are_returned(self, repo_stub):
802 model = VcsSettingsModel()
805 model = VcsSettingsModel()
803 result = model.get_global_general_settings()
806 result = model.get_global_general_settings()
804 expected_result = model.global_settings.get_all_settings()
807 expected_result = model.global_settings.get_all_settings()
805 assert sorted(result) == sorted(expected_result)
808 assert sorted(result) == sorted(expected_result)
806
809
807 def test_repo_uis_are_not_overriding_global_uis(
810 def test_repo_uis_are_not_overriding_global_uis(
808 self, repo_stub, settings_util):
811 self, repo_stub, settings_util):
809 for key in VcsSettingsModel.GENERAL_SETTINGS:
812 for key in VcsSettingsModel.GENERAL_SETTINGS:
810 settings_util.create_repo_rhodecode_setting(
813 settings_util.create_repo_rhodecode_setting(
811 repo_stub, key, 'abcde', type_='unicode')
814 repo_stub, key, 'abcde', type_='unicode')
812 model = VcsSettingsModel(repo=repo_stub.repo_name)
815 model = VcsSettingsModel(repo=repo_stub.repo_name)
813 result = model.get_global_general_settings()
816 result = model.get_global_general_settings()
814 expected_result = model.global_settings.get_all_settings()
817 expected_result = model.global_settings.get_all_settings()
815 assert sorted(result) == sorted(expected_result)
818 assert sorted(result) == sorted(expected_result)
816
819
817
820
818 class TestGetGlobalUiSettings(object):
821 class TestGetGlobalUiSettings(object):
819 def test_global_uis_are_returned(self, repo_stub):
822 def test_global_uis_are_returned(self, repo_stub):
820 model = VcsSettingsModel()
823 model = VcsSettingsModel()
821 result = model.get_global_ui_settings()
824 result = model.get_global_ui_settings()
822 expected_result = model.global_settings.get_ui()
825 expected_result = model.global_settings.get_ui()
823 assert sorted(result) == sorted(expected_result)
826 assert sorted(result) == sorted(expected_result)
824
827
825 def test_repo_uis_are_not_overriding_global_uis(
828 def test_repo_uis_are_not_overriding_global_uis(
826 self, repo_stub, settings_util):
829 self, repo_stub, settings_util):
827 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
830 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
828 settings_util.create_repo_rhodecode_ui(
831 settings_util.create_repo_rhodecode_ui(
829 repo_stub, section, 'repo', key=key, active=False)
832 repo_stub, section, 'repo', key=key, active=False)
830 model = VcsSettingsModel(repo=repo_stub.repo_name)
833 model = VcsSettingsModel(repo=repo_stub.repo_name)
831 result = model.get_global_ui_settings()
834 result = model.get_global_ui_settings()
832 expected_result = model.global_settings.get_ui()
835 expected_result = model.global_settings.get_ui()
833 assert sorted(result) == sorted(expected_result)
836 assert sorted(result) == sorted(expected_result)
834
837
835 def test_ui_settings_filtered_by_section(
838 def test_ui_settings_filtered_by_section(
836 self, repo_stub, settings_util):
839 self, repo_stub, settings_util):
837 model = VcsSettingsModel(repo=repo_stub.repo_name)
840 model = VcsSettingsModel(repo=repo_stub.repo_name)
838 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
841 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
839 result = model.get_global_ui_settings(section=section)
842 result = model.get_global_ui_settings(section=section)
840 expected_result = model.global_settings.get_ui(section=section)
843 expected_result = model.global_settings.get_ui(section=section)
841 assert sorted(result) == sorted(expected_result)
844 assert sorted(result) == sorted(expected_result)
842
845
843 def test_ui_settings_filtered_by_key(
846 def test_ui_settings_filtered_by_key(
844 self, repo_stub, settings_util):
847 self, repo_stub, settings_util):
845 model = VcsSettingsModel(repo=repo_stub.repo_name)
848 model = VcsSettingsModel(repo=repo_stub.repo_name)
846 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
849 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
847 result = model.get_global_ui_settings(key=key)
850 result = model.get_global_ui_settings(key=key)
848 expected_result = model.global_settings.get_ui(key=key)
851 expected_result = model.global_settings.get_ui(key=key)
849 assert sorted(result) == sorted(expected_result)
852 assert sorted(result) == sorted(expected_result)
850
853
851
854
852 class TestGetGeneralSettings(object):
855 class TestGetGeneralSettings(object):
853 def test_global_settings_are_returned_when_inherited_is_true(
856 def test_global_settings_are_returned_when_inherited_is_true(
854 self, repo_stub, settings_util):
857 self, repo_stub, settings_util):
855 model = VcsSettingsModel(repo=repo_stub.repo_name)
858 model = VcsSettingsModel(repo=repo_stub.repo_name)
856 model.inherit_global_settings = True
859 model.inherit_global_settings = True
857 for key in VcsSettingsModel.GENERAL_SETTINGS:
860 for key in VcsSettingsModel.GENERAL_SETTINGS:
858 settings_util.create_repo_rhodecode_setting(
861 settings_util.create_repo_rhodecode_setting(
859 repo_stub, key, 'abcde', type_='unicode')
862 repo_stub, key, 'abcde', type_='unicode')
860 result = model.get_general_settings()
863 result = model.get_general_settings()
861 expected_result = model.get_global_general_settings()
864 expected_result = model.get_global_general_settings()
862 assert sorted(result) == sorted(expected_result)
865 assert sorted(result) == sorted(expected_result)
863
866
864 def test_repo_settings_are_returned_when_inherited_is_false(
867 def test_repo_settings_are_returned_when_inherited_is_false(
865 self, repo_stub, settings_util):
868 self, repo_stub, settings_util):
866 model = VcsSettingsModel(repo=repo_stub.repo_name)
869 model = VcsSettingsModel(repo=repo_stub.repo_name)
867 model.inherit_global_settings = False
870 model.inherit_global_settings = False
868 for key in VcsSettingsModel.GENERAL_SETTINGS:
871 for key in VcsSettingsModel.GENERAL_SETTINGS:
869 settings_util.create_repo_rhodecode_setting(
872 settings_util.create_repo_rhodecode_setting(
870 repo_stub, key, 'abcde', type_='unicode')
873 repo_stub, key, 'abcde', type_='unicode')
871 result = model.get_general_settings()
874 result = model.get_general_settings()
872 expected_result = model.get_repo_general_settings()
875 expected_result = model.get_repo_general_settings()
873 assert sorted(result) == sorted(expected_result)
876 assert sorted(result) == sorted(expected_result)
874
877
875 def test_global_settings_are_returned_when_no_repository_specified(self):
878 def test_global_settings_are_returned_when_no_repository_specified(self):
876 model = VcsSettingsModel()
879 model = VcsSettingsModel()
877 result = model.get_general_settings()
880 result = model.get_general_settings()
878 expected_result = model.get_global_general_settings()
881 expected_result = model.get_global_general_settings()
879 assert sorted(result) == sorted(expected_result)
882 assert sorted(result) == sorted(expected_result)
880
883
881
884
882 class TestGetUiSettings(object):
885 class TestGetUiSettings(object):
883 def test_global_settings_are_returned_when_inherited_is_true(
886 def test_global_settings_are_returned_when_inherited_is_true(
884 self, repo_stub, settings_util):
887 self, repo_stub, settings_util):
885 model = VcsSettingsModel(repo=repo_stub.repo_name)
888 model = VcsSettingsModel(repo=repo_stub.repo_name)
886 model.inherit_global_settings = True
889 model.inherit_global_settings = True
887 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
890 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
888 settings_util.create_repo_rhodecode_ui(
891 settings_util.create_repo_rhodecode_ui(
889 repo_stub, section, 'repo', key=key, active=True)
892 repo_stub, section, 'repo', key=key, active=True)
890 result = model.get_ui_settings()
893 result = model.get_ui_settings()
891 expected_result = model.get_global_ui_settings()
894 expected_result = model.get_global_ui_settings()
892 assert sorted(result) == sorted(expected_result)
895 assert sorted(result) == sorted(expected_result)
893
896
894 def test_repo_settings_are_returned_when_inherited_is_false(
897 def test_repo_settings_are_returned_when_inherited_is_false(
895 self, repo_stub, settings_util):
898 self, repo_stub, settings_util):
896 model = VcsSettingsModel(repo=repo_stub.repo_name)
899 model = VcsSettingsModel(repo=repo_stub.repo_name)
897 model.inherit_global_settings = False
900 model.inherit_global_settings = False
898 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
901 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
899 settings_util.create_repo_rhodecode_ui(
902 settings_util.create_repo_rhodecode_ui(
900 repo_stub, section, 'repo', key=key, active=True)
903 repo_stub, section, 'repo', key=key, active=True)
901 result = model.get_ui_settings()
904 result = model.get_ui_settings()
902 expected_result = model.get_repo_ui_settings()
905 expected_result = model.get_repo_ui_settings()
903 assert sorted(result) == sorted(expected_result)
906 assert sorted(result) == sorted(expected_result)
904
907
905 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
908 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
906 model = VcsSettingsModel(repo=repo_stub.repo_name)
909 model = VcsSettingsModel(repo=repo_stub.repo_name)
907 model.inherit_global_settings = False
910 model.inherit_global_settings = False
908 args = ('section', 'key')
911 args = ('section', 'key')
909 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
912 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
910 model.get_ui_settings(*args)
913 model.get_ui_settings(*args)
911 settings_mock.assert_called_once_with(*args)
914 settings_mock.assert_called_once_with(*args)
912
915
913 def test_global_settings_filtered_by_section_and_key(self):
916 def test_global_settings_filtered_by_section_and_key(self):
914 model = VcsSettingsModel()
917 model = VcsSettingsModel()
915 args = ('section', 'key')
918 args = ('section', 'key')
916 with mock.patch.object(model, 'get_global_ui_settings') as (
919 with mock.patch.object(model, 'get_global_ui_settings') as (
917 settings_mock):
920 settings_mock):
918 model.get_ui_settings(*args)
921 model.get_ui_settings(*args)
919 settings_mock.assert_called_once_with(*args)
922 settings_mock.assert_called_once_with(*args)
920
923
921 def test_global_settings_are_returned_when_no_repository_specified(self):
924 def test_global_settings_are_returned_when_no_repository_specified(self):
922 model = VcsSettingsModel()
925 model = VcsSettingsModel()
923 result = model.get_ui_settings()
926 result = model.get_ui_settings()
924 expected_result = model.get_global_ui_settings()
927 expected_result = model.get_global_ui_settings()
925 assert sorted(result) == sorted(expected_result)
928 assert sorted(result) == sorted(expected_result)
926
929
927
930
928 class TestGetSvnPatterns(object):
931 class TestGetSvnPatterns(object):
929 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
932 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
930 model = VcsSettingsModel(repo=repo_stub.repo_name)
933 model = VcsSettingsModel(repo=repo_stub.repo_name)
931 args = ('section', )
934 args = ('section', )
932 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
935 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
933 model.get_svn_patterns(*args)
936 model.get_svn_patterns(*args)
934 settings_mock.assert_called_once_with(*args)
937 settings_mock.assert_called_once_with(*args)
935
938
936 def test_global_settings_filtered_by_section_and_key(self):
939 def test_global_settings_filtered_by_section_and_key(self):
937 model = VcsSettingsModel()
940 model = VcsSettingsModel()
938 args = ('section', )
941 args = ('section', )
939 with mock.patch.object(model, 'get_global_ui_settings') as (
942 with mock.patch.object(model, 'get_global_ui_settings') as (
940 settings_mock):
943 settings_mock):
941 model.get_svn_patterns(*args)
944 model.get_svn_patterns(*args)
942 settings_mock.assert_called_once_with(*args)
945 settings_mock.assert_called_once_with(*args)
943
946
944
947
945 class TestGetReposLocation(object):
948 class TestGetReposLocation(object):
946 def test_returns_repos_location(self, repo_stub):
949 def test_returns_repos_location(self, repo_stub):
947 model = VcsSettingsModel()
950 model = VcsSettingsModel()
948
951
949 result_mock = mock.Mock()
952 result_mock = mock.Mock()
950 result_mock.ui_value = '/tmp'
953 result_mock.ui_value = '/tmp'
951
954
952 with mock.patch.object(model, 'global_settings') as settings_mock:
955 with mock.patch.object(model, 'global_settings') as settings_mock:
953 settings_mock.get_ui_by_key.return_value = result_mock
956 settings_mock.get_ui_by_key.return_value = result_mock
954 result = model.get_repos_location()
957 result = model.get_repos_location()
955
958
956 settings_mock.get_ui_by_key.assert_called_once_with('/')
959 settings_mock.get_ui_by_key.assert_called_once_with('/')
957 assert result == '/tmp'
960 assert result == '/tmp'
958
961
959
962
960 class TestCreateOrUpdateRepoSettings(object):
963 class TestCreateOrUpdateRepoSettings(object):
961 FORM_DATA = {
964 FORM_DATA = {
962 'inherit_global_settings': False,
965 'inherit_global_settings': False,
963 'hooks_changegroup_repo_size': False,
966 'hooks_changegroup_repo_size': False,
964 'hooks_changegroup_push_logger': False,
967 'hooks_changegroup_push_logger': False,
965 'hooks_outgoing_pull_logger': False,
968 'hooks_outgoing_pull_logger': False,
966 'extensions_largefiles': False,
969 'extensions_largefiles': False,
967 'extensions_evolve': False,
970 'extensions_evolve': False,
968 'largefiles_usercache': '/example/largefiles-store',
971 'largefiles_usercache': '/example/largefiles-store',
969 'vcs_git_lfs_enabled': False,
972 'vcs_git_lfs_enabled': False,
970 'vcs_git_lfs_store_location': '/',
973 'vcs_git_lfs_store_location': '/',
971 'phases_publish': 'False',
974 'phases_publish': 'False',
972 'rhodecode_pr_merge_enabled': False,
975 'rhodecode_pr_merge_enabled': False,
973 'rhodecode_use_outdated_comments': False,
976 'rhodecode_use_outdated_comments': False,
974 'new_svn_branch': '',
977 'new_svn_branch': '',
975 'new_svn_tag': ''
978 'new_svn_tag': ''
976 }
979 }
977
980
978 def test_get_raises_exception_when_repository_not_specified(self):
981 def test_get_raises_exception_when_repository_not_specified(self):
979 model = VcsSettingsModel()
982 model = VcsSettingsModel()
980 with pytest.raises(Exception) as exc_info:
983 with pytest.raises(Exception) as exc_info:
981 model.create_or_update_repo_settings(data=self.FORM_DATA)
984 model.create_or_update_repo_settings(data=self.FORM_DATA)
982 assert exc_info.value.message == 'Repository is not specified'
985 assert exc_info.value.message == 'Repository is not specified'
983
986
984 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
987 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
985 repo = backend_svn.create_repo()
988 repo = backend_svn.create_repo()
986 model = VcsSettingsModel(repo=repo)
989 model = VcsSettingsModel(repo=repo)
987 with self._patch_model(model) as mocks:
990 with self._patch_model(model) as mocks:
988 model.create_or_update_repo_settings(
991 model.create_or_update_repo_settings(
989 data=self.FORM_DATA, inherit_global_settings=False)
992 data=self.FORM_DATA, inherit_global_settings=False)
990 mocks['create_repo_svn_settings'].assert_called_once_with(
993 mocks['create_repo_svn_settings'].assert_called_once_with(
991 self.FORM_DATA)
994 self.FORM_DATA)
992 non_called_methods = (
995 non_called_methods = (
993 'create_or_update_repo_hook_settings',
996 'create_or_update_repo_hook_settings',
994 'create_or_update_repo_pr_settings',
997 'create_or_update_repo_pr_settings',
995 'create_or_update_repo_hg_settings')
998 'create_or_update_repo_hg_settings')
996 for method in non_called_methods:
999 for method in non_called_methods:
997 assert mocks[method].call_count == 0
1000 assert mocks[method].call_count == 0
998
1001
999 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1002 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
1000 repo = backend_hg.create_repo()
1003 repo = backend_hg.create_repo()
1001 model = VcsSettingsModel(repo=repo)
1004 model = VcsSettingsModel(repo=repo)
1002 with self._patch_model(model) as mocks:
1005 with self._patch_model(model) as mocks:
1003 model.create_or_update_repo_settings(
1006 model.create_or_update_repo_settings(
1004 data=self.FORM_DATA, inherit_global_settings=False)
1007 data=self.FORM_DATA, inherit_global_settings=False)
1005
1008
1006 assert mocks['create_repo_svn_settings'].call_count == 0
1009 assert mocks['create_repo_svn_settings'].call_count == 0
1007 called_methods = (
1010 called_methods = (
1008 'create_or_update_repo_hook_settings',
1011 'create_or_update_repo_hook_settings',
1009 'create_or_update_repo_pr_settings',
1012 'create_or_update_repo_pr_settings',
1010 'create_or_update_repo_hg_settings')
1013 'create_or_update_repo_hg_settings')
1011 for method in called_methods:
1014 for method in called_methods:
1012 mocks[method].assert_called_once_with(self.FORM_DATA)
1015 mocks[method].assert_called_once_with(self.FORM_DATA)
1013
1016
1014 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1017 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
1015 self, backend_git):
1018 self, backend_git):
1016 repo = backend_git.create_repo()
1019 repo = backend_git.create_repo()
1017 model = VcsSettingsModel(repo=repo)
1020 model = VcsSettingsModel(repo=repo)
1018 with self._patch_model(model) as mocks:
1021 with self._patch_model(model) as mocks:
1019 model.create_or_update_repo_settings(
1022 model.create_or_update_repo_settings(
1020 data=self.FORM_DATA, inherit_global_settings=False)
1023 data=self.FORM_DATA, inherit_global_settings=False)
1021
1024
1022 assert mocks['create_repo_svn_settings'].call_count == 0
1025 assert mocks['create_repo_svn_settings'].call_count == 0
1023 called_methods = (
1026 called_methods = (
1024 'create_or_update_repo_hook_settings',
1027 'create_or_update_repo_hook_settings',
1025 'create_or_update_repo_pr_settings')
1028 'create_or_update_repo_pr_settings')
1026 non_called_methods = (
1029 non_called_methods = (
1027 'create_repo_svn_settings',
1030 'create_repo_svn_settings',
1028 'create_or_update_repo_hg_settings'
1031 'create_or_update_repo_hg_settings'
1029 )
1032 )
1030 for method in called_methods:
1033 for method in called_methods:
1031 mocks[method].assert_called_once_with(self.FORM_DATA)
1034 mocks[method].assert_called_once_with(self.FORM_DATA)
1032 for method in non_called_methods:
1035 for method in non_called_methods:
1033 assert mocks[method].call_count == 0
1036 assert mocks[method].call_count == 0
1034
1037
1035 def test_no_methods_are_called_when_settings_are_inherited(
1038 def test_no_methods_are_called_when_settings_are_inherited(
1036 self, backend):
1039 self, backend):
1037 repo = backend.create_repo()
1040 repo = backend.create_repo()
1038 model = VcsSettingsModel(repo=repo)
1041 model = VcsSettingsModel(repo=repo)
1039 with self._patch_model(model) as mocks:
1042 with self._patch_model(model) as mocks:
1040 model.create_or_update_repo_settings(
1043 model.create_or_update_repo_settings(
1041 data=self.FORM_DATA, inherit_global_settings=True)
1044 data=self.FORM_DATA, inherit_global_settings=True)
1042 for method_name in mocks:
1045 for method_name in mocks:
1043 assert mocks[method_name].call_count == 0
1046 assert mocks[method_name].call_count == 0
1044
1047
1045 def test_cache_is_marked_for_invalidation(self, repo_stub):
1048 def test_cache_is_marked_for_invalidation(self, repo_stub):
1046 model = VcsSettingsModel(repo=repo_stub)
1049 model = VcsSettingsModel(repo=repo_stub)
1047 invalidation_patcher = mock.patch(
1050 invalidation_patcher = mock.patch(
1048 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1051 'rhodecode.model.scm.ScmModel.mark_for_invalidation')
1049 with invalidation_patcher as invalidation_mock:
1052 with invalidation_patcher as invalidation_mock:
1050 model.create_or_update_repo_settings(
1053 model.create_or_update_repo_settings(
1051 data=self.FORM_DATA, inherit_global_settings=True)
1054 data=self.FORM_DATA, inherit_global_settings=True)
1052 invalidation_mock.assert_called_once_with(
1055 invalidation_mock.assert_called_once_with(
1053 repo_stub.repo_name, delete=True)
1056 repo_stub.repo_name, delete=True)
1054
1057
1055 def test_inherit_flag_is_saved(self, repo_stub):
1058 def test_inherit_flag_is_saved(self, repo_stub):
1056 model = VcsSettingsModel(repo=repo_stub)
1059 model = VcsSettingsModel(repo=repo_stub)
1057 model.inherit_global_settings = True
1060 model.inherit_global_settings = True
1058 with self._patch_model(model):
1061 with self._patch_model(model):
1059 model.create_or_update_repo_settings(
1062 model.create_or_update_repo_settings(
1060 data=self.FORM_DATA, inherit_global_settings=False)
1063 data=self.FORM_DATA, inherit_global_settings=False)
1061 assert model.inherit_global_settings is False
1064 assert model.inherit_global_settings is False
1062
1065
1063 def _patch_model(self, model):
1066 def _patch_model(self, model):
1064 return mock.patch.multiple(
1067 return mock.patch.multiple(
1065 model,
1068 model,
1066 create_repo_svn_settings=mock.DEFAULT,
1069 create_repo_svn_settings=mock.DEFAULT,
1067 create_or_update_repo_hook_settings=mock.DEFAULT,
1070 create_or_update_repo_hook_settings=mock.DEFAULT,
1068 create_or_update_repo_pr_settings=mock.DEFAULT,
1071 create_or_update_repo_pr_settings=mock.DEFAULT,
1069 create_or_update_repo_hg_settings=mock.DEFAULT)
1072 create_or_update_repo_hg_settings=mock.DEFAULT)
@@ -1,859 +1,859 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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 mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 @pytest.mark.usefixtures('config_stub')
44 @pytest.mark.usefixtures('config_stub')
45 class TestPullRequestModel(object):
45 class TestPullRequestModel(object):
46
46
47 @pytest.fixture
47 @pytest.fixture
48 def pull_request(self, request, backend, pr_util):
48 def pull_request(self, request, backend, pr_util):
49 """
49 """
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
58
58
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
61 self.comment_patcher = mock.patch(
61 self.comment_patcher = mock.patch(
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 self.comment_patcher.start()
63 self.comment_patcher.start()
64 self.notification_patcher = mock.patch(
64 self.notification_patcher = mock.patch(
65 'rhodecode.model.notification.NotificationModel.create')
65 'rhodecode.model.notification.NotificationModel.create')
66 self.notification_patcher.start()
66 self.notification_patcher.start()
67 self.helper_patcher = mock.patch(
67 self.helper_patcher = mock.patch(
68 'rhodecode.lib.helpers.url')
68 'rhodecode.lib.helpers.url')
69 self.helper_patcher.start()
69 self.helper_patcher.start()
70
70
71 self.hook_patcher = mock.patch.object(PullRequestModel,
71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 '_trigger_pull_request_hook')
72 '_trigger_pull_request_hook')
73 self.hook_mock = self.hook_patcher.start()
73 self.hook_mock = self.hook_patcher.start()
74
74
75 self.invalidation_patcher = mock.patch(
75 self.invalidation_patcher = mock.patch(
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 self.invalidation_mock = self.invalidation_patcher.start()
77 self.invalidation_mock = self.invalidation_patcher.start()
78
78
79 self.pull_request = pr_util.create_pull_request(
79 self.pull_request = pr_util.create_pull_request(
80 mergeable=True, name_suffix=u'Δ…Δ‡')
80 mergeable=True, name_suffix=u'Δ…Δ‡')
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84
84
85 @request.addfinalizer
85 @request.addfinalizer
86 def cleanup_pull_request():
86 def cleanup_pull_request():
87 calls = [mock.call(
87 calls = [mock.call(
88 self.pull_request, self.pull_request.author, 'create')]
88 self.pull_request, self.pull_request.author, 'create')]
89 self.hook_mock.assert_has_calls(calls)
89 self.hook_mock.assert_has_calls(calls)
90
90
91 self.workspace_remove_patcher.stop()
91 self.workspace_remove_patcher.stop()
92 self.merge_patcher.stop()
92 self.merge_patcher.stop()
93 self.comment_patcher.stop()
93 self.comment_patcher.stop()
94 self.notification_patcher.stop()
94 self.notification_patcher.stop()
95 self.helper_patcher.stop()
95 self.helper_patcher.stop()
96 self.hook_patcher.stop()
96 self.hook_patcher.stop()
97 self.invalidation_patcher.stop()
97 self.invalidation_patcher.stop()
98
98
99 return self.pull_request
99 return self.pull_request
100
100
101 def test_get_all(self, pull_request):
101 def test_get_all(self, pull_request):
102 prs = PullRequestModel().get_all(pull_request.target_repo)
102 prs = PullRequestModel().get_all(pull_request.target_repo)
103 assert isinstance(prs, list)
103 assert isinstance(prs, list)
104 assert len(prs) == 1
104 assert len(prs) == 1
105
105
106 def test_count_all(self, pull_request):
106 def test_count_all(self, pull_request):
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
108 assert pr_count == 1
108 assert pr_count == 1
109
109
110 def test_get_awaiting_review(self, pull_request):
110 def test_get_awaiting_review(self, pull_request):
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
112 assert isinstance(prs, list)
112 assert isinstance(prs, list)
113 assert len(prs) == 1
113 assert len(prs) == 1
114
114
115 def test_count_awaiting_review(self, pull_request):
115 def test_count_awaiting_review(self, pull_request):
116 pr_count = PullRequestModel().count_awaiting_review(
116 pr_count = PullRequestModel().count_awaiting_review(
117 pull_request.target_repo)
117 pull_request.target_repo)
118 assert pr_count == 1
118 assert pr_count == 1
119
119
120 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
121 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False)],
123 pull_request.author)
123 pull_request.author)
124 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
125 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
126 assert isinstance(prs, list)
126 assert isinstance(prs, list)
127 assert len(prs) == 1
127 assert len(prs) == 1
128
128
129 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
130 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False)],
132 pull_request.author)
132 pull_request.author)
133 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
134 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
135 assert pr_count == 1
135 assert pr_count == 1
136
136
137 def test_delete_calls_cleanup_merge(self, pull_request):
137 def test_delete_calls_cleanup_merge(self, pull_request):
138 PullRequestModel().delete(pull_request, pull_request.author)
138 PullRequestModel().delete(pull_request, pull_request.author)
139
139
140 self.workspace_remove_mock.assert_called_once_with(
140 self.workspace_remove_mock.assert_called_once_with(
141 self.workspace_id)
141 self.workspace_id)
142
142
143 def test_close_calls_cleanup_and_hook(self, pull_request):
143 def test_close_calls_cleanup_and_hook(self, pull_request):
144 PullRequestModel().close_pull_request(
144 PullRequestModel().close_pull_request(
145 pull_request, pull_request.author)
145 pull_request, pull_request.author)
146
146
147 self.workspace_remove_mock.assert_called_once_with(
147 self.workspace_remove_mock.assert_called_once_with(
148 self.workspace_id)
148 self.workspace_id)
149 self.hook_mock.assert_called_with(
149 self.hook_mock.assert_called_with(
150 self.pull_request, self.pull_request.author, 'close')
150 self.pull_request, self.pull_request.author, 'close')
151
151
152 def test_merge_status(self, pull_request):
152 def test_merge_status(self, pull_request):
153 self.merge_mock.return_value = MergeResponse(
153 self.merge_mock.return_value = MergeResponse(
154 True, False, None, MergeFailureReason.NONE)
154 True, False, None, MergeFailureReason.NONE)
155
155
156 assert pull_request._last_merge_source_rev is None
156 assert pull_request._last_merge_source_rev is None
157 assert pull_request._last_merge_target_rev is None
157 assert pull_request._last_merge_target_rev is None
158 assert pull_request.last_merge_status is None
158 assert pull_request.last_merge_status is None
159
159
160 status, msg = PullRequestModel().merge_status(pull_request)
160 status, msg = PullRequestModel().merge_status(pull_request)
161 assert status is True
161 assert status is True
162 assert msg.eval() == 'This pull request can be automatically merged.'
162 assert msg.eval() == 'This pull request can be automatically merged.'
163 self.merge_mock.assert_called_once_with(
163 self.merge_mock.assert_called_once_with(
164 pull_request.target_ref_parts,
164 pull_request.target_ref_parts,
165 pull_request.source_repo.scm_instance(),
165 pull_request.source_repo.scm_instance(),
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
167 use_rebase=False)
167 use_rebase=False, close_branch=False)
168
168
169 assert pull_request._last_merge_source_rev == self.source_commit
169 assert pull_request._last_merge_source_rev == self.source_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
172
172
173 self.merge_mock.reset_mock()
173 self.merge_mock.reset_mock()
174 status, msg = PullRequestModel().merge_status(pull_request)
174 status, msg = PullRequestModel().merge_status(pull_request)
175 assert status is True
175 assert status is True
176 assert msg.eval() == 'This pull request can be automatically merged.'
176 assert msg.eval() == 'This pull request can be automatically merged.'
177 assert self.merge_mock.called is False
177 assert self.merge_mock.called is False
178
178
179 def test_merge_status_known_failure(self, pull_request):
179 def test_merge_status_known_failure(self, pull_request):
180 self.merge_mock.return_value = MergeResponse(
180 self.merge_mock.return_value = MergeResponse(
181 False, False, None, MergeFailureReason.MERGE_FAILED)
181 False, False, None, MergeFailureReason.MERGE_FAILED)
182
182
183 assert pull_request._last_merge_source_rev is None
183 assert pull_request._last_merge_source_rev is None
184 assert pull_request._last_merge_target_rev is None
184 assert pull_request._last_merge_target_rev is None
185 assert pull_request.last_merge_status is None
185 assert pull_request.last_merge_status is None
186
186
187 status, msg = PullRequestModel().merge_status(pull_request)
187 status, msg = PullRequestModel().merge_status(pull_request)
188 assert status is False
188 assert status is False
189 assert (
189 assert (
190 msg.eval() ==
190 msg.eval() ==
191 'This pull request cannot be merged because of merge conflicts.')
191 'This pull request cannot be merged because of merge conflicts.')
192 self.merge_mock.assert_called_once_with(
192 self.merge_mock.assert_called_once_with(
193 pull_request.target_ref_parts,
193 pull_request.target_ref_parts,
194 pull_request.source_repo.scm_instance(),
194 pull_request.source_repo.scm_instance(),
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
196 use_rebase=False)
196 use_rebase=False, close_branch=False)
197
197
198 assert pull_request._last_merge_source_rev == self.source_commit
198 assert pull_request._last_merge_source_rev == self.source_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
200 assert (
200 assert (
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
202
202
203 self.merge_mock.reset_mock()
203 self.merge_mock.reset_mock()
204 status, msg = PullRequestModel().merge_status(pull_request)
204 status, msg = PullRequestModel().merge_status(pull_request)
205 assert status is False
205 assert status is False
206 assert (
206 assert (
207 msg.eval() ==
207 msg.eval() ==
208 'This pull request cannot be merged because of merge conflicts.')
208 'This pull request cannot be merged because of merge conflicts.')
209 assert self.merge_mock.called is False
209 assert self.merge_mock.called is False
210
210
211 def test_merge_status_unknown_failure(self, pull_request):
211 def test_merge_status_unknown_failure(self, pull_request):
212 self.merge_mock.return_value = MergeResponse(
212 self.merge_mock.return_value = MergeResponse(
213 False, False, None, MergeFailureReason.UNKNOWN)
213 False, False, None, MergeFailureReason.UNKNOWN)
214
214
215 assert pull_request._last_merge_source_rev is None
215 assert pull_request._last_merge_source_rev is None
216 assert pull_request._last_merge_target_rev is None
216 assert pull_request._last_merge_target_rev is None
217 assert pull_request.last_merge_status is None
217 assert pull_request.last_merge_status is None
218
218
219 status, msg = PullRequestModel().merge_status(pull_request)
219 status, msg = PullRequestModel().merge_status(pull_request)
220 assert status is False
220 assert status is False
221 assert msg.eval() == (
221 assert msg.eval() == (
222 'This pull request cannot be merged because of an unhandled'
222 'This pull request cannot be merged because of an unhandled'
223 ' exception.')
223 ' exception.')
224 self.merge_mock.assert_called_once_with(
224 self.merge_mock.assert_called_once_with(
225 pull_request.target_ref_parts,
225 pull_request.target_ref_parts,
226 pull_request.source_repo.scm_instance(),
226 pull_request.source_repo.scm_instance(),
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
228 use_rebase=False)
228 use_rebase=False, close_branch=False)
229
229
230 assert pull_request._last_merge_source_rev is None
230 assert pull_request._last_merge_source_rev is None
231 assert pull_request._last_merge_target_rev is None
231 assert pull_request._last_merge_target_rev is None
232 assert pull_request.last_merge_status is None
232 assert pull_request.last_merge_status is None
233
233
234 self.merge_mock.reset_mock()
234 self.merge_mock.reset_mock()
235 status, msg = PullRequestModel().merge_status(pull_request)
235 status, msg = PullRequestModel().merge_status(pull_request)
236 assert status is False
236 assert status is False
237 assert msg.eval() == (
237 assert msg.eval() == (
238 'This pull request cannot be merged because of an unhandled'
238 'This pull request cannot be merged because of an unhandled'
239 ' exception.')
239 ' exception.')
240 assert self.merge_mock.called is True
240 assert self.merge_mock.called is True
241
241
242 def test_merge_status_when_target_is_locked(self, pull_request):
242 def test_merge_status_when_target_is_locked(self, pull_request):
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
244 status, msg = PullRequestModel().merge_status(pull_request)
244 status, msg = PullRequestModel().merge_status(pull_request)
245 assert status is False
245 assert status is False
246 assert msg.eval() == (
246 assert msg.eval() == (
247 'This pull request cannot be merged because the target repository'
247 'This pull request cannot be merged because the target repository'
248 ' is locked.')
248 ' is locked.')
249
249
250 def test_merge_status_requirements_check_target(self, pull_request):
250 def test_merge_status_requirements_check_target(self, pull_request):
251
251
252 def has_largefiles(self, repo):
252 def has_largefiles(self, repo):
253 return repo == pull_request.source_repo
253 return repo == pull_request.source_repo
254
254
255 patcher = mock.patch.object(
255 patcher = mock.patch.object(
256 PullRequestModel, '_has_largefiles', has_largefiles)
256 PullRequestModel, '_has_largefiles', has_largefiles)
257 with patcher:
257 with patcher:
258 status, msg = PullRequestModel().merge_status(pull_request)
258 status, msg = PullRequestModel().merge_status(pull_request)
259
259
260 assert status is False
260 assert status is False
261 assert msg == 'Target repository large files support is disabled.'
261 assert msg == 'Target repository large files support is disabled.'
262
262
263 def test_merge_status_requirements_check_source(self, pull_request):
263 def test_merge_status_requirements_check_source(self, pull_request):
264
264
265 def has_largefiles(self, repo):
265 def has_largefiles(self, repo):
266 return repo == pull_request.target_repo
266 return repo == pull_request.target_repo
267
267
268 patcher = mock.patch.object(
268 patcher = mock.patch.object(
269 PullRequestModel, '_has_largefiles', has_largefiles)
269 PullRequestModel, '_has_largefiles', has_largefiles)
270 with patcher:
270 with patcher:
271 status, msg = PullRequestModel().merge_status(pull_request)
271 status, msg = PullRequestModel().merge_status(pull_request)
272
272
273 assert status is False
273 assert status is False
274 assert msg == 'Source repository large files support is disabled.'
274 assert msg == 'Source repository large files support is disabled.'
275
275
276 def test_merge(self, pull_request, merge_extras):
276 def test_merge(self, pull_request, merge_extras):
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
278 merge_ref = Reference(
278 merge_ref = Reference(
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
280 self.merge_mock.return_value = MergeResponse(
280 self.merge_mock.return_value = MergeResponse(
281 True, True, merge_ref, MergeFailureReason.NONE)
281 True, True, merge_ref, MergeFailureReason.NONE)
282
282
283 merge_extras['repository'] = pull_request.target_repo.repo_name
283 merge_extras['repository'] = pull_request.target_repo.repo_name
284 PullRequestModel().merge(
284 PullRequestModel().merge(
285 pull_request, pull_request.author, extras=merge_extras)
285 pull_request, pull_request.author, extras=merge_extras)
286
286
287 message = (
287 message = (
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
289 u'\n\n {pr_title}'.format(
289 u'\n\n {pr_title}'.format(
290 pr_id=pull_request.pull_request_id,
290 pr_id=pull_request.pull_request_id,
291 source_repo=safe_unicode(
291 source_repo=safe_unicode(
292 pull_request.source_repo.scm_instance().name),
292 pull_request.source_repo.scm_instance().name),
293 source_ref_name=pull_request.source_ref_parts.name,
293 source_ref_name=pull_request.source_ref_parts.name,
294 pr_title=safe_unicode(pull_request.title)
294 pr_title=safe_unicode(pull_request.title)
295 )
295 )
296 )
296 )
297 self.merge_mock.assert_called_once_with(
297 self.merge_mock.assert_called_once_with(
298 pull_request.target_ref_parts,
298 pull_request.target_ref_parts,
299 pull_request.source_repo.scm_instance(),
299 pull_request.source_repo.scm_instance(),
300 pull_request.source_ref_parts, self.workspace_id,
300 pull_request.source_ref_parts, self.workspace_id,
301 user_name=user.username, user_email=user.email, message=message,
301 user_name=user.username, user_email=user.email, message=message,
302 use_rebase=False
302 use_rebase=False, close_branch=False
303 )
303 )
304 self.invalidation_mock.assert_called_once_with(
304 self.invalidation_mock.assert_called_once_with(
305 pull_request.target_repo.repo_name)
305 pull_request.target_repo.repo_name)
306
306
307 self.hook_mock.assert_called_with(
307 self.hook_mock.assert_called_with(
308 self.pull_request, self.pull_request.author, 'merge')
308 self.pull_request, self.pull_request.author, 'merge')
309
309
310 pull_request = PullRequest.get(pull_request.pull_request_id)
310 pull_request = PullRequest.get(pull_request.pull_request_id)
311 assert (
311 assert (
312 pull_request.merge_rev ==
312 pull_request.merge_rev ==
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314
314
315 def test_merge_failed(self, pull_request, merge_extras):
315 def test_merge_failed(self, pull_request, merge_extras):
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
317 merge_ref = Reference(
317 merge_ref = Reference(
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
319 self.merge_mock.return_value = MergeResponse(
319 self.merge_mock.return_value = MergeResponse(
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
321
321
322 merge_extras['repository'] = pull_request.target_repo.repo_name
322 merge_extras['repository'] = pull_request.target_repo.repo_name
323 PullRequestModel().merge(
323 PullRequestModel().merge(
324 pull_request, pull_request.author, extras=merge_extras)
324 pull_request, pull_request.author, extras=merge_extras)
325
325
326 message = (
326 message = (
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
328 u'\n\n {pr_title}'.format(
328 u'\n\n {pr_title}'.format(
329 pr_id=pull_request.pull_request_id,
329 pr_id=pull_request.pull_request_id,
330 source_repo=safe_unicode(
330 source_repo=safe_unicode(
331 pull_request.source_repo.scm_instance().name),
331 pull_request.source_repo.scm_instance().name),
332 source_ref_name=pull_request.source_ref_parts.name,
332 source_ref_name=pull_request.source_ref_parts.name,
333 pr_title=safe_unicode(pull_request.title)
333 pr_title=safe_unicode(pull_request.title)
334 )
334 )
335 )
335 )
336 self.merge_mock.assert_called_once_with(
336 self.merge_mock.assert_called_once_with(
337 pull_request.target_ref_parts,
337 pull_request.target_ref_parts,
338 pull_request.source_repo.scm_instance(),
338 pull_request.source_repo.scm_instance(),
339 pull_request.source_ref_parts, self.workspace_id,
339 pull_request.source_ref_parts, self.workspace_id,
340 user_name=user.username, user_email=user.email, message=message,
340 user_name=user.username, user_email=user.email, message=message,
341 use_rebase=False
341 use_rebase=False, close_branch=False
342 )
342 )
343
343
344 pull_request = PullRequest.get(pull_request.pull_request_id)
344 pull_request = PullRequest.get(pull_request.pull_request_id)
345 assert self.invalidation_mock.called is False
345 assert self.invalidation_mock.called is False
346 assert pull_request.merge_rev is None
346 assert pull_request.merge_rev is None
347
347
348 def test_get_commit_ids(self, pull_request):
348 def test_get_commit_ids(self, pull_request):
349 # The PR has been not merget yet, so expect an exception
349 # The PR has been not merget yet, so expect an exception
350 with pytest.raises(ValueError):
350 with pytest.raises(ValueError):
351 PullRequestModel()._get_commit_ids(pull_request)
351 PullRequestModel()._get_commit_ids(pull_request)
352
352
353 # Merge revision is in the revisions list
353 # Merge revision is in the revisions list
354 pull_request.merge_rev = pull_request.revisions[0]
354 pull_request.merge_rev = pull_request.revisions[0]
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions
356 assert commit_ids == pull_request.revisions
357
357
358 # Merge revision is not in the revisions list
358 # Merge revision is not in the revisions list
359 pull_request.merge_rev = 'f000' * 10
359 pull_request.merge_rev = 'f000' * 10
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
362
362
363 def test_get_diff_from_pr_version(self, pull_request):
363 def test_get_diff_from_pr_version(self, pull_request):
364 source_repo = pull_request.source_repo
364 source_repo = pull_request.source_repo
365 source_ref_id = pull_request.source_ref_parts.commit_id
365 source_ref_id = pull_request.source_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
368 source_repo, source_ref_id, target_ref_id, context=6)
368 source_repo, source_ref_id, target_ref_id, context=6)
369 assert 'file_1' in diff.raw
369 assert 'file_1' in diff.raw
370
370
371 def test_generate_title_returns_unicode(self):
371 def test_generate_title_returns_unicode(self):
372 title = PullRequestModel().generate_pullrequest_title(
372 title = PullRequestModel().generate_pullrequest_title(
373 source='source-dummy',
373 source='source-dummy',
374 source_ref='source-ref-dummy',
374 source_ref='source-ref-dummy',
375 target='target-dummy',
375 target='target-dummy',
376 )
376 )
377 assert type(title) == unicode
377 assert type(title) == unicode
378
378
379
379
380 @pytest.mark.usefixtures('config_stub')
380 @pytest.mark.usefixtures('config_stub')
381 class TestIntegrationMerge(object):
381 class TestIntegrationMerge(object):
382 @pytest.mark.parametrize('extra_config', (
382 @pytest.mark.parametrize('extra_config', (
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
384 ))
384 ))
385 def test_merge_triggers_push_hooks(
385 def test_merge_triggers_push_hooks(
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
387 extra_config):
387 extra_config):
388 pull_request = pr_util.create_pull_request(
388 pull_request = pr_util.create_pull_request(
389 approved=True, mergeable=True)
389 approved=True, mergeable=True)
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
391 merge_extras['repository'] = pull_request.target_repo.repo_name
391 merge_extras['repository'] = pull_request.target_repo.repo_name
392 Session().commit()
392 Session().commit()
393
393
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
395 merge_state = PullRequestModel().merge(
395 merge_state = PullRequestModel().merge(
396 pull_request, user_admin, extras=merge_extras)
396 pull_request, user_admin, extras=merge_extras)
397
397
398 assert merge_state.executed
398 assert merge_state.executed
399 assert 'pre_push' in capture_rcextensions
399 assert 'pre_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
401
401
402 def test_merge_can_be_rejected_by_pre_push_hook(
402 def test_merge_can_be_rejected_by_pre_push_hook(
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
404 pull_request = pr_util.create_pull_request(
404 pull_request = pr_util.create_pull_request(
405 approved=True, mergeable=True)
405 approved=True, mergeable=True)
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
407 merge_extras['repository'] = pull_request.target_repo.repo_name
407 merge_extras['repository'] = pull_request.target_repo.repo_name
408 Session().commit()
408 Session().commit()
409
409
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
411 pre_pull.side_effect = RepositoryError("Disallow push!")
411 pre_pull.side_effect = RepositoryError("Disallow push!")
412 merge_status = PullRequestModel().merge(
412 merge_status = PullRequestModel().merge(
413 pull_request, user_admin, extras=merge_extras)
413 pull_request, user_admin, extras=merge_extras)
414
414
415 assert not merge_status.executed
415 assert not merge_status.executed
416 assert 'pre_push' not in capture_rcextensions
416 assert 'pre_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
418
418
419 def test_merge_fails_if_target_is_locked(
419 def test_merge_fails_if_target_is_locked(
420 self, pr_util, user_regular, merge_extras):
420 self, pr_util, user_regular, merge_extras):
421 pull_request = pr_util.create_pull_request(
421 pull_request = pr_util.create_pull_request(
422 approved=True, mergeable=True)
422 approved=True, mergeable=True)
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
424 pull_request.target_repo.locked = locked_by
424 pull_request.target_repo.locked = locked_by
425 # TODO: johbo: Check if this can work based on the database, currently
425 # TODO: johbo: Check if this can work based on the database, currently
426 # all data is pre-computed, that's why just updating the DB is not
426 # all data is pre-computed, that's why just updating the DB is not
427 # enough.
427 # enough.
428 merge_extras['locked_by'] = locked_by
428 merge_extras['locked_by'] = locked_by
429 merge_extras['repository'] = pull_request.target_repo.repo_name
429 merge_extras['repository'] = pull_request.target_repo.repo_name
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
431 Session().commit()
431 Session().commit()
432 merge_status = PullRequestModel().merge(
432 merge_status = PullRequestModel().merge(
433 pull_request, user_regular, extras=merge_extras)
433 pull_request, user_regular, extras=merge_extras)
434 assert not merge_status.executed
434 assert not merge_status.executed
435
435
436
436
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
438 (False, 1, 0),
438 (False, 1, 0),
439 (True, 0, 1),
439 (True, 0, 1),
440 ])
440 ])
441 def test_outdated_comments(
441 def test_outdated_comments(
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
443 pull_request = pr_util.create_pull_request()
443 pull_request = pr_util.create_pull_request()
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
445
445
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
447 pr_util.add_one_commit()
447 pr_util.add_one_commit()
448 assert_inline_comments(
448 assert_inline_comments(
449 pull_request, visible=inlines_count, outdated=outdated_count)
449 pull_request, visible=inlines_count, outdated=outdated_count)
450 outdated_comment_mock.assert_called_with(pull_request)
450 outdated_comment_mock.assert_called_with(pull_request)
451
451
452
452
453 @pytest.fixture
453 @pytest.fixture
454 def merge_extras(user_regular):
454 def merge_extras(user_regular):
455 """
455 """
456 Context for the vcs operation when running a merge.
456 Context for the vcs operation when running a merge.
457 """
457 """
458 extras = {
458 extras = {
459 'ip': '127.0.0.1',
459 'ip': '127.0.0.1',
460 'username': user_regular.username,
460 'username': user_regular.username,
461 'action': 'push',
461 'action': 'push',
462 'repository': 'fake_target_repo_name',
462 'repository': 'fake_target_repo_name',
463 'scm': 'git',
463 'scm': 'git',
464 'config': 'fake_config_ini_path',
464 'config': 'fake_config_ini_path',
465 'make_lock': None,
465 'make_lock': None,
466 'locked_by': [None, None, None],
466 'locked_by': [None, None, None],
467 'server_url': 'http://test.example.com:5000',
467 'server_url': 'http://test.example.com:5000',
468 'hooks': ['push', 'pull'],
468 'hooks': ['push', 'pull'],
469 'is_shadow_repo': False,
469 'is_shadow_repo': False,
470 }
470 }
471 return extras
471 return extras
472
472
473
473
474 @pytest.mark.usefixtures('config_stub')
474 @pytest.mark.usefixtures('config_stub')
475 class TestUpdateCommentHandling(object):
475 class TestUpdateCommentHandling(object):
476
476
477 @pytest.fixture(autouse=True, scope='class')
477 @pytest.fixture(autouse=True, scope='class')
478 def enable_outdated_comments(self, request, pylonsapp):
478 def enable_outdated_comments(self, request, pylonsapp):
479 config_patch = mock.patch.dict(
479 config_patch = mock.patch.dict(
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
481 config_patch.start()
481 config_patch.start()
482
482
483 @request.addfinalizer
483 @request.addfinalizer
484 def cleanup():
484 def cleanup():
485 config_patch.stop()
485 config_patch.stop()
486
486
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
488 commits = [
488 commits = [
489 {'message': 'a'},
489 {'message': 'a'},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
492 ]
492 ]
493 pull_request = pr_util.create_pull_request(
493 pull_request = pr_util.create_pull_request(
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
495 pr_util.create_inline_comment(file_path='file_b')
495 pr_util.create_inline_comment(file_path='file_b')
496 pr_util.add_one_commit(head='c')
496 pr_util.add_one_commit(head='c')
497
497
498 assert_inline_comments(pull_request, visible=1, outdated=0)
498 assert_inline_comments(pull_request, visible=1, outdated=0)
499
499
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
501 original_content = ''.join(
501 original_content = ''.join(
502 ['line {}\n'.format(x) for x in range(1, 11)])
502 ['line {}\n'.format(x) for x in range(1, 11)])
503 updated_content = 'new_line_at_top\n' + original_content
503 updated_content = 'new_line_at_top\n' + original_content
504 commits = [
504 commits = [
505 {'message': 'a'},
505 {'message': 'a'},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
508 ]
508 ]
509 pull_request = pr_util.create_pull_request(
509 pull_request = pr_util.create_pull_request(
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
511
511
512 with outdated_comments_patcher():
512 with outdated_comments_patcher():
513 comment = pr_util.create_inline_comment(
513 comment = pr_util.create_inline_comment(
514 line_no=u'n8', file_path='file_b')
514 line_no=u'n8', file_path='file_b')
515 pr_util.add_one_commit(head='c')
515 pr_util.add_one_commit(head='c')
516
516
517 assert_inline_comments(pull_request, visible=1, outdated=0)
517 assert_inline_comments(pull_request, visible=1, outdated=0)
518 assert comment.line_no == u'n9'
518 assert comment.line_no == u'n9'
519
519
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
522 updated_content = original_content + 'new_line_at_end\n'
522 updated_content = original_content + 'new_line_at_end\n'
523 commits = [
523 commits = [
524 {'message': 'a'},
524 {'message': 'a'},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
527 ]
527 ]
528 pull_request = pr_util.create_pull_request(
528 pull_request = pr_util.create_pull_request(
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
530 pr_util.create_inline_comment(file_path='file_b')
530 pr_util.create_inline_comment(file_path='file_b')
531 pr_util.add_one_commit(head='c')
531 pr_util.add_one_commit(head='c')
532
532
533 assert_inline_comments(pull_request, visible=1, outdated=0)
533 assert_inline_comments(pull_request, visible=1, outdated=0)
534
534
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
538 change_lines = list(base_lines)
538 change_lines = list(base_lines)
539 change_lines.insert(6, 'line 6a added\n')
539 change_lines.insert(6, 'line 6a added\n')
540
540
541 # Changes on the last line of sight
541 # Changes on the last line of sight
542 update_lines = list(change_lines)
542 update_lines = list(change_lines)
543 update_lines[0] = 'line 1 changed\n'
543 update_lines[0] = 'line 1 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
545
545
546 def file_b(lines):
546 def file_b(lines):
547 return FileNode('file_b', ''.join(lines))
547 return FileNode('file_b', ''.join(lines))
548
548
549 commits = [
549 commits = [
550 {'message': 'a', 'added': [file_b(base_lines)]},
550 {'message': 'a', 'added': [file_b(base_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
553 ]
553 ]
554
554
555 pull_request = pr_util.create_pull_request(
555 pull_request = pr_util.create_pull_request(
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
558
558
559 with outdated_comments_patcher():
559 with outdated_comments_patcher():
560 pr_util.add_one_commit(head='c')
560 pr_util.add_one_commit(head='c')
561 assert_inline_comments(pull_request, visible=0, outdated=1)
561 assert_inline_comments(pull_request, visible=0, outdated=1)
562
562
563 @pytest.mark.parametrize("change, content", [
563 @pytest.mark.parametrize("change, content", [
564 ('changed', 'changed\n'),
564 ('changed', 'changed\n'),
565 ('removed', ''),
565 ('removed', ''),
566 ], ids=['changed', 'removed'])
566 ], ids=['changed', 'removed'])
567 def test_comment_flagged_on_change(self, pr_util, change, content):
567 def test_comment_flagged_on_change(self, pr_util, change, content):
568 commits = [
568 commits = [
569 {'message': 'a'},
569 {'message': 'a'},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
572 ]
572 ]
573 pull_request = pr_util.create_pull_request(
573 pull_request = pr_util.create_pull_request(
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
575 pr_util.create_inline_comment(file_path='file_b')
575 pr_util.create_inline_comment(file_path='file_b')
576
576
577 with outdated_comments_patcher():
577 with outdated_comments_patcher():
578 pr_util.add_one_commit(head='c')
578 pr_util.add_one_commit(head='c')
579 assert_inline_comments(pull_request, visible=0, outdated=1)
579 assert_inline_comments(pull_request, visible=0, outdated=1)
580
580
581
581
582 @pytest.mark.usefixtures('config_stub')
582 @pytest.mark.usefixtures('config_stub')
583 class TestUpdateChangedFiles(object):
583 class TestUpdateChangedFiles(object):
584
584
585 def test_no_changes_on_unchanged_diff(self, pr_util):
585 def test_no_changes_on_unchanged_diff(self, pr_util):
586 commits = [
586 commits = [
587 {'message': 'a'},
587 {'message': 'a'},
588 {'message': 'b',
588 {'message': 'b',
589 'added': [FileNode('file_b', 'test_content b\n')]},
589 'added': [FileNode('file_b', 'test_content b\n')]},
590 {'message': 'c',
590 {'message': 'c',
591 'added': [FileNode('file_c', 'test_content c\n')]},
591 'added': [FileNode('file_c', 'test_content c\n')]},
592 ]
592 ]
593 # open a PR from a to b, adding file_b
593 # open a PR from a to b, adding file_b
594 pull_request = pr_util.create_pull_request(
594 pull_request = pr_util.create_pull_request(
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
596 name_suffix='per-file-review')
596 name_suffix='per-file-review')
597
597
598 # modify PR adding new file file_c
598 # modify PR adding new file file_c
599 pr_util.add_one_commit(head='c')
599 pr_util.add_one_commit(head='c')
600
600
601 assert_pr_file_changes(
601 assert_pr_file_changes(
602 pull_request,
602 pull_request,
603 added=['file_c'],
603 added=['file_c'],
604 modified=[],
604 modified=[],
605 removed=[])
605 removed=[])
606
606
607 def test_modify_and_undo_modification_diff(self, pr_util):
607 def test_modify_and_undo_modification_diff(self, pr_util):
608 commits = [
608 commits = [
609 {'message': 'a'},
609 {'message': 'a'},
610 {'message': 'b',
610 {'message': 'b',
611 'added': [FileNode('file_b', 'test_content b\n')]},
611 'added': [FileNode('file_b', 'test_content b\n')]},
612 {'message': 'c',
612 {'message': 'c',
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
614 {'message': 'd',
614 {'message': 'd',
615 'changed': [FileNode('file_b', 'test_content b\n')]},
615 'changed': [FileNode('file_b', 'test_content b\n')]},
616 ]
616 ]
617 # open a PR from a to b, adding file_b
617 # open a PR from a to b, adding file_b
618 pull_request = pr_util.create_pull_request(
618 pull_request = pr_util.create_pull_request(
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
620 name_suffix='per-file-review')
620 name_suffix='per-file-review')
621
621
622 # modify PR modifying file file_b
622 # modify PR modifying file file_b
623 pr_util.add_one_commit(head='c')
623 pr_util.add_one_commit(head='c')
624
624
625 assert_pr_file_changes(
625 assert_pr_file_changes(
626 pull_request,
626 pull_request,
627 added=[],
627 added=[],
628 modified=['file_b'],
628 modified=['file_b'],
629 removed=[])
629 removed=[])
630
630
631 # move the head again to d, which rollbacks change,
631 # move the head again to d, which rollbacks change,
632 # meaning we should indicate no changes
632 # meaning we should indicate no changes
633 pr_util.add_one_commit(head='d')
633 pr_util.add_one_commit(head='d')
634
634
635 assert_pr_file_changes(
635 assert_pr_file_changes(
636 pull_request,
636 pull_request,
637 added=[],
637 added=[],
638 modified=[],
638 modified=[],
639 removed=[])
639 removed=[])
640
640
641 def test_updated_all_files_in_pr(self, pr_util):
641 def test_updated_all_files_in_pr(self, pr_util):
642 commits = [
642 commits = [
643 {'message': 'a'},
643 {'message': 'a'},
644 {'message': 'b', 'added': [
644 {'message': 'b', 'added': [
645 FileNode('file_a', 'test_content a\n'),
645 FileNode('file_a', 'test_content a\n'),
646 FileNode('file_b', 'test_content b\n'),
646 FileNode('file_b', 'test_content b\n'),
647 FileNode('file_c', 'test_content c\n')]},
647 FileNode('file_c', 'test_content c\n')]},
648 {'message': 'c', 'changed': [
648 {'message': 'c', 'changed': [
649 FileNode('file_a', 'test_content a changed\n'),
649 FileNode('file_a', 'test_content a changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
651 FileNode('file_c', 'test_content c changed\n')]},
651 FileNode('file_c', 'test_content c changed\n')]},
652 ]
652 ]
653 # open a PR from a to b, changing 3 files
653 # open a PR from a to b, changing 3 files
654 pull_request = pr_util.create_pull_request(
654 pull_request = pr_util.create_pull_request(
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
656 name_suffix='per-file-review')
656 name_suffix='per-file-review')
657
657
658 pr_util.add_one_commit(head='c')
658 pr_util.add_one_commit(head='c')
659
659
660 assert_pr_file_changes(
660 assert_pr_file_changes(
661 pull_request,
661 pull_request,
662 added=[],
662 added=[],
663 modified=['file_a', 'file_b', 'file_c'],
663 modified=['file_a', 'file_b', 'file_c'],
664 removed=[])
664 removed=[])
665
665
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
667 commits = [
667 commits = [
668 {'message': 'a'},
668 {'message': 'a'},
669 {'message': 'b', 'added': [
669 {'message': 'b', 'added': [
670 FileNode('file_a', 'test_content a\n'),
670 FileNode('file_a', 'test_content a\n'),
671 FileNode('file_b', 'test_content b\n'),
671 FileNode('file_b', 'test_content b\n'),
672 FileNode('file_c', 'test_content c\n')]},
672 FileNode('file_c', 'test_content c\n')]},
673 {'message': 'c', 'removed': [
673 {'message': 'c', 'removed': [
674 FileNode('file_a', 'test_content a changed\n'),
674 FileNode('file_a', 'test_content a changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
676 FileNode('file_c', 'test_content c changed\n')]},
676 FileNode('file_c', 'test_content c changed\n')]},
677 ]
677 ]
678 # open a PR from a to b, removing 3 files
678 # open a PR from a to b, removing 3 files
679 pull_request = pr_util.create_pull_request(
679 pull_request = pr_util.create_pull_request(
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
681 name_suffix='per-file-review')
681 name_suffix='per-file-review')
682
682
683 pr_util.add_one_commit(head='c')
683 pr_util.add_one_commit(head='c')
684
684
685 assert_pr_file_changes(
685 assert_pr_file_changes(
686 pull_request,
686 pull_request,
687 added=[],
687 added=[],
688 modified=[],
688 modified=[],
689 removed=['file_a', 'file_b', 'file_c'])
689 removed=['file_a', 'file_b', 'file_c'])
690
690
691
691
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
693 model = PullRequestModel()
693 model = PullRequestModel()
694 pull_request = pr_util.create_pull_request()
694 pull_request = pr_util.create_pull_request()
695 pr_util.update_source_repository()
695 pr_util.update_source_repository()
696
696
697 model.update_commits(pull_request)
697 model.update_commits(pull_request)
698
698
699 # Expect that it has a version entry now
699 # Expect that it has a version entry now
700 assert len(model.get_versions(pull_request)) == 1
700 assert len(model.get_versions(pull_request)) == 1
701
701
702
702
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
704 pull_request = pr_util.create_pull_request()
704 pull_request = pr_util.create_pull_request()
705 model = PullRequestModel()
705 model = PullRequestModel()
706 model.update_commits(pull_request)
706 model.update_commits(pull_request)
707
707
708 # Expect that it still has no versions
708 # Expect that it still has no versions
709 assert len(model.get_versions(pull_request)) == 0
709 assert len(model.get_versions(pull_request)) == 0
710
710
711
711
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
713 model = PullRequestModel()
713 model = PullRequestModel()
714 pull_request = pr_util.create_pull_request()
714 pull_request = pr_util.create_pull_request()
715 comment = pr_util.create_comment()
715 comment = pr_util.create_comment()
716 pr_util.update_source_repository()
716 pr_util.update_source_repository()
717
717
718 model.update_commits(pull_request)
718 model.update_commits(pull_request)
719
719
720 # Expect that the comment is linked to the pr version now
720 # Expect that the comment is linked to the pr version now
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
722
722
723
723
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
725 model = PullRequestModel()
725 model = PullRequestModel()
726 pull_request = pr_util.create_pull_request()
726 pull_request = pr_util.create_pull_request()
727 pr_util.update_source_repository()
727 pr_util.update_source_repository()
728 pr_util.update_source_repository()
728 pr_util.update_source_repository()
729
729
730 model.update_commits(pull_request)
730 model.update_commits(pull_request)
731
731
732 # Expect to find a new comment about the change
732 # Expect to find a new comment about the change
733 expected_message = textwrap.dedent(
733 expected_message = textwrap.dedent(
734 """\
734 """\
735 Pull request updated. Auto status change to |under_review|
735 Pull request updated. Auto status change to |under_review|
736
736
737 .. role:: added
737 .. role:: added
738 .. role:: removed
738 .. role:: removed
739 .. parsed-literal::
739 .. parsed-literal::
740
740
741 Changed commits:
741 Changed commits:
742 * :added:`1 added`
742 * :added:`1 added`
743 * :removed:`0 removed`
743 * :removed:`0 removed`
744
744
745 Changed files:
745 Changed files:
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
747
747
748 .. |under_review| replace:: *"Under Review"*"""
748 .. |under_review| replace:: *"Under Review"*"""
749 )
749 )
750 pull_request_comments = sorted(
750 pull_request_comments = sorted(
751 pull_request.comments, key=lambda c: c.modified_at)
751 pull_request.comments, key=lambda c: c.modified_at)
752 update_comment = pull_request_comments[-1]
752 update_comment = pull_request_comments[-1]
753 assert update_comment.text == expected_message
753 assert update_comment.text == expected_message
754
754
755
755
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
757 pull_request = pr_util.create_pull_request()
757 pull_request = pr_util.create_pull_request()
758
758
759 # Avoiding default values
759 # Avoiding default values
760 pull_request.status = PullRequest.STATUS_CLOSED
760 pull_request.status = PullRequest.STATUS_CLOSED
761 pull_request._last_merge_source_rev = "0" * 40
761 pull_request._last_merge_source_rev = "0" * 40
762 pull_request._last_merge_target_rev = "1" * 40
762 pull_request._last_merge_target_rev = "1" * 40
763 pull_request.last_merge_status = 1
763 pull_request.last_merge_status = 1
764 pull_request.merge_rev = "2" * 40
764 pull_request.merge_rev = "2" * 40
765
765
766 # Remember automatic values
766 # Remember automatic values
767 created_on = pull_request.created_on
767 created_on = pull_request.created_on
768 updated_on = pull_request.updated_on
768 updated_on = pull_request.updated_on
769
769
770 # Create a new version of the pull request
770 # Create a new version of the pull request
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
772
772
773 # Check attributes
773 # Check attributes
774 assert version.title == pr_util.create_parameters['title']
774 assert version.title == pr_util.create_parameters['title']
775 assert version.description == pr_util.create_parameters['description']
775 assert version.description == pr_util.create_parameters['description']
776 assert version.status == PullRequest.STATUS_CLOSED
776 assert version.status == PullRequest.STATUS_CLOSED
777
777
778 # versions get updated created_on
778 # versions get updated created_on
779 assert version.created_on != created_on
779 assert version.created_on != created_on
780
780
781 assert version.updated_on == updated_on
781 assert version.updated_on == updated_on
782 assert version.user_id == pull_request.user_id
782 assert version.user_id == pull_request.user_id
783 assert version.revisions == pr_util.create_parameters['revisions']
783 assert version.revisions == pr_util.create_parameters['revisions']
784 assert version.source_repo == pr_util.source_repository
784 assert version.source_repo == pr_util.source_repository
785 assert version.source_ref == pr_util.create_parameters['source_ref']
785 assert version.source_ref == pr_util.create_parameters['source_ref']
786 assert version.target_repo == pr_util.target_repository
786 assert version.target_repo == pr_util.target_repository
787 assert version.target_ref == pr_util.create_parameters['target_ref']
787 assert version.target_ref == pr_util.create_parameters['target_ref']
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
790 assert version.last_merge_status == pull_request.last_merge_status
790 assert version.last_merge_status == pull_request.last_merge_status
791 assert version.merge_rev == pull_request.merge_rev
791 assert version.merge_rev == pull_request.merge_rev
792 assert version.pull_request == pull_request
792 assert version.pull_request == pull_request
793
793
794
794
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
796 version1 = pr_util.create_version_of_pull_request()
796 version1 = pr_util.create_version_of_pull_request()
797 comment_linked = pr_util.create_comment(linked_to=version1)
797 comment_linked = pr_util.create_comment(linked_to=version1)
798 comment_unlinked = pr_util.create_comment()
798 comment_unlinked = pr_util.create_comment()
799 version2 = pr_util.create_version_of_pull_request()
799 version2 = pr_util.create_version_of_pull_request()
800
800
801 PullRequestModel()._link_comments_to_version(version2)
801 PullRequestModel()._link_comments_to_version(version2)
802
802
803 # Expect that only the new comment is linked to version2
803 # Expect that only the new comment is linked to version2
804 assert (
804 assert (
805 comment_unlinked.pull_request_version_id ==
805 comment_unlinked.pull_request_version_id ==
806 version2.pull_request_version_id)
806 version2.pull_request_version_id)
807 assert (
807 assert (
808 comment_linked.pull_request_version_id ==
808 comment_linked.pull_request_version_id ==
809 version1.pull_request_version_id)
809 version1.pull_request_version_id)
810 assert (
810 assert (
811 comment_unlinked.pull_request_version_id !=
811 comment_unlinked.pull_request_version_id !=
812 comment_linked.pull_request_version_id)
812 comment_linked.pull_request_version_id)
813
813
814
814
815 def test_calculate_commits():
815 def test_calculate_commits():
816 old_ids = [1, 2, 3]
816 old_ids = [1, 2, 3]
817 new_ids = [1, 3, 4, 5]
817 new_ids = [1, 3, 4, 5]
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
819 assert change.added == [4, 5]
819 assert change.added == [4, 5]
820 assert change.common == [1, 3]
820 assert change.common == [1, 3]
821 assert change.removed == [2]
821 assert change.removed == [2]
822 assert change.total == [1, 3, 4, 5]
822 assert change.total == [1, 3, 4, 5]
823
823
824
824
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
826 if visible is not None:
826 if visible is not None:
827 inline_comments = CommentsModel().get_inline_comments(
827 inline_comments = CommentsModel().get_inline_comments(
828 pull_request.target_repo.repo_id, pull_request=pull_request)
828 pull_request.target_repo.repo_id, pull_request=pull_request)
829 inline_cnt = CommentsModel().get_inline_comments_count(
829 inline_cnt = CommentsModel().get_inline_comments_count(
830 inline_comments)
830 inline_comments)
831 assert inline_cnt == visible
831 assert inline_cnt == visible
832 if outdated is not None:
832 if outdated is not None:
833 outdated_comments = CommentsModel().get_outdated_comments(
833 outdated_comments = CommentsModel().get_outdated_comments(
834 pull_request.target_repo.repo_id, pull_request)
834 pull_request.target_repo.repo_id, pull_request)
835 assert len(outdated_comments) == outdated
835 assert len(outdated_comments) == outdated
836
836
837
837
838 def assert_pr_file_changes(
838 def assert_pr_file_changes(
839 pull_request, added=None, modified=None, removed=None):
839 pull_request, added=None, modified=None, removed=None):
840 pr_versions = PullRequestModel().get_versions(pull_request)
840 pr_versions = PullRequestModel().get_versions(pull_request)
841 # always use first version, ie original PR to calculate changes
841 # always use first version, ie original PR to calculate changes
842 pull_request_version = pr_versions[0]
842 pull_request_version = pr_versions[0]
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
844 pull_request, pull_request_version)
844 pull_request, pull_request_version)
845 file_changes = PullRequestModel()._calculate_file_changes(
845 file_changes = PullRequestModel()._calculate_file_changes(
846 old_diff_data, new_diff_data)
846 old_diff_data, new_diff_data)
847
847
848 assert added == file_changes.added, \
848 assert added == file_changes.added, \
849 'expected added:%s vs value:%s' % (added, file_changes.added)
849 'expected added:%s vs value:%s' % (added, file_changes.added)
850 assert modified == file_changes.modified, \
850 assert modified == file_changes.modified, \
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
852 assert removed == file_changes.removed, \
852 assert removed == file_changes.removed, \
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
854
854
855
855
856 def outdated_comments_patcher(use_outdated=True):
856 def outdated_comments_patcher(use_outdated=True):
857 return mock.patch.object(
857 return mock.patch.object(
858 CommentsModel, 'use_outdated_comments',
858 CommentsModel, 'use_outdated_comments',
859 return_value=use_outdated)
859 return_value=use_outdated)
General Comments 0
You need to be logged in to leave comments. Login now