##// END OF EJS Templates
tests: fixed compare page and related tests....
marcink -
r3773:a77b6fa8 new-ui
parent child Browse files
Show More
@@ -1,666 +1,666 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23 import lxml.html
24 24
25 25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 26 from rhodecode.tests import assert_session_flash
27 27 from rhodecode.tests.utils import AssertResponse, commit_change
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 31 import urllib
32 32
33 33 base_url = {
34 34 'repo_compare_select': '/{repo_name}/compare',
35 35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 36 }[name].format(**kwargs)
37 37
38 38 if params:
39 39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 40 return base_url
41 41
42 42
43 43 @pytest.mark.usefixtures("autologin_user", "app")
44 44 class TestCompareView(object):
45 45
46 46 def test_compare_index_is_reached_at_least_once(self, backend):
47 47 repo = backend.repo
48 48 self.app.get(
49 49 route_path('repo_compare_select', repo_name=repo.repo_name))
50 50
51 51 @pytest.mark.xfail_backends("svn", reason="Requires pull")
52 52 def test_compare_remote_with_different_commit_indexes(self, backend):
53 53 # Preparing the following repository structure:
54 54 #
55 55 # Origin repository has two commits:
56 56 #
57 57 # 0 1
58 58 # A -- D
59 59 #
60 60 # The fork of it has a few more commits and "D" has a commit index
61 61 # which does not exist in origin.
62 62 #
63 63 # 0 1 2 3 4
64 64 # A -- -- -- D -- E
65 65 # \- B -- C
66 66 #
67 67
68 68 fork = backend.create_repo()
69 69
70 70 # prepare fork
71 71 commit0 = commit_change(
72 72 fork.repo_name, filename='file1', content='A',
73 73 message='A', vcs_type=backend.alias, parent=None, newfile=True)
74 74
75 75 commit1 = commit_change(
76 76 fork.repo_name, filename='file1', content='B',
77 77 message='B, child of A', vcs_type=backend.alias, parent=commit0)
78 78
79 79 commit_change( # commit 2
80 80 fork.repo_name, filename='file1', content='C',
81 81 message='C, child of B', vcs_type=backend.alias, parent=commit1)
82 82
83 83 commit3 = commit_change(
84 84 fork.repo_name, filename='file1', content='D',
85 85 message='D, child of A', vcs_type=backend.alias, parent=commit0)
86 86
87 87 commit4 = commit_change(
88 88 fork.repo_name, filename='file1', content='E',
89 89 message='E, child of D', vcs_type=backend.alias, parent=commit3)
90 90
91 91 # prepare origin repository, taking just the history up to D
92 92 origin = backend.create_repo()
93 93
94 94 origin_repo = origin.scm_instance(cache=False)
95 95 origin_repo.config.clear_section('hooks')
96 96 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
97 origin_repo = origin.scm_instance(cache=False) # cache rebuild
97 98
98 99 # Verify test fixture setup
99 100 # This does not work for git
100 101 if backend.alias != 'git':
101 102 assert 5 == len(fork.scm_instance().commit_ids)
102 103 assert 2 == len(origin_repo.commit_ids)
103 104
104 105 # Comparing the revisions
105 106 response = self.app.get(
106 107 route_path('repo_compare',
107 108 repo_name=origin.repo_name,
108 109 source_ref_type="rev", source_ref=commit3.raw_id,
109 110 target_ref_type="rev", target_ref=commit4.raw_id,
110 111 params=dict(merge='1', target_repo=fork.repo_name)
111 112 ))
112 113
113 114 compare_page = ComparePage(response)
114 115 compare_page.contains_commits([commit4])
115 116
116 117 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
117 118 def test_compare_forks_on_branch_extra_commits(self, backend):
118 119 repo1 = backend.create_repo()
119 120
120 121 # commit something !
121 122 commit0 = commit_change(
122 123 repo1.repo_name, filename='file1', content='line1\n',
123 124 message='commit1', vcs_type=backend.alias, parent=None,
124 125 newfile=True)
125 126
126 127 # fork this repo
127 128 repo2 = backend.create_fork()
128 129
129 130 # add two extra commit into fork
130 131 commit1 = commit_change(
131 132 repo2.repo_name, filename='file1', content='line1\nline2\n',
132 133 message='commit2', vcs_type=backend.alias, parent=commit0)
133 134
134 135 commit2 = commit_change(
135 136 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
136 137 message='commit3', vcs_type=backend.alias, parent=commit1)
137 138
138 139 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
139 140 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
140 141
141 142 response = self.app.get(
142 143 route_path('repo_compare',
143 144 repo_name=repo1.repo_name,
144 145 source_ref_type="branch", source_ref=commit_id2,
145 146 target_ref_type="branch", target_ref=commit_id1,
146 147 params=dict(merge='1', target_repo=repo2.repo_name)
147 148 ))
148 149
149 150 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
150 151 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
151 152
152 153 compare_page = ComparePage(response)
153 154 compare_page.contains_change_summary(1, 2, 0)
154 155 compare_page.contains_commits([commit1, commit2])
155 156
156 157 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
157 158 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
158 159
159 160 # Swap is removed when comparing branches since it's a PR feature and
160 161 # it is then a preview mode
161 162 compare_page.swap_is_hidden()
162 163 compare_page.target_source_are_disabled()
163 164
164 165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(
166 self, backend):
166 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
167 167 repo1 = backend.create_repo()
168 168
169 169 # commit something !
170 170 commit0 = commit_change(
171 171 repo1.repo_name, filename='file1', content='line1\n',
172 172 message='commit1', vcs_type=backend.alias, parent=None,
173 173 newfile=True)
174 174
175 175 # fork this repo
176 176 repo2 = backend.create_fork()
177 177
178 178 # now commit something to origin repo
179 179 commit_change(
180 180 repo1.repo_name, filename='file2', content='line1file2\n',
181 181 message='commit2', vcs_type=backend.alias, parent=commit0,
182 182 newfile=True)
183 183
184 184 # add two extra commit into fork
185 185 commit1 = commit_change(
186 186 repo2.repo_name, filename='file1', content='line1\nline2\n',
187 187 message='commit2', vcs_type=backend.alias, parent=commit0)
188 188
189 189 commit2 = commit_change(
190 190 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
191 191 message='commit3', vcs_type=backend.alias, parent=commit1)
192 192
193 193 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
194 194 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
195 195
196 196 response = self.app.get(
197 197 route_path('repo_compare',
198 198 repo_name=repo1.repo_name,
199 199 source_ref_type="branch", source_ref=commit_id2,
200 200 target_ref_type="branch", target_ref=commit_id1,
201 201 params=dict(merge='1', target_repo=repo2.repo_name),
202 202 ))
203 203
204 204 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
205 205 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
206 206
207 207 compare_page = ComparePage(response)
208 208 compare_page.contains_change_summary(1, 2, 0)
209 209 compare_page.contains_commits([commit1, commit2])
210 210 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
211 211 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
212 212
213 213 # Swap is removed when comparing branches since it's a PR feature and
214 214 # it is then a preview mode
215 215 compare_page.swap_is_hidden()
216 216 compare_page.target_source_are_disabled()
217 217
218 218 @pytest.mark.xfail_backends("svn")
219 219 # TODO(marcink): no svn support for compare two seperate repos
220 220 def test_compare_of_unrelated_forks(self, backend):
221 221 orig = backend.create_repo(number_of_commits=1)
222 222 fork = backend.create_repo(number_of_commits=1)
223 223
224 224 response = self.app.get(
225 225 route_path('repo_compare',
226 226 repo_name=orig.repo_name,
227 227 source_ref_type="rev", source_ref="tip",
228 228 target_ref_type="rev", target_ref="tip",
229 229 params=dict(merge='1', target_repo=fork.repo_name),
230 230 ),
231 231 status=302)
232 232 response = response.follow()
233 233 response.mustcontain("Repositories unrelated.")
234 234
235 235 @pytest.mark.xfail_backends("svn")
236 236 def test_compare_cherry_pick_commits_from_bottom(self, backend):
237 237
238 238 # repo1:
239 239 # commit0:
240 240 # commit1:
241 241 # repo1-fork- in which we will cherry pick bottom commits
242 242 # commit0:
243 243 # commit1:
244 244 # commit2: x
245 245 # commit3: x
246 246 # commit4: x
247 247 # commit5:
248 248 # make repo1, and commit1+commit2
249 249
250 250 repo1 = backend.create_repo()
251 251
252 252 # commit something !
253 253 commit0 = commit_change(
254 254 repo1.repo_name, filename='file1', content='line1\n',
255 255 message='commit1', vcs_type=backend.alias, parent=None,
256 256 newfile=True)
257 257 commit1 = commit_change(
258 258 repo1.repo_name, filename='file1', content='line1\nline2\n',
259 259 message='commit2', vcs_type=backend.alias, parent=commit0)
260 260
261 261 # fork this repo
262 262 repo2 = backend.create_fork()
263 263
264 264 # now make commit3-6
265 265 commit2 = commit_change(
266 266 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
267 267 message='commit3', vcs_type=backend.alias, parent=commit1)
268 268 commit3 = commit_change(
269 269 repo1.repo_name, filename='file1',
270 270 content='line1\nline2\nline3\nline4\n', message='commit4',
271 271 vcs_type=backend.alias, parent=commit2)
272 272 commit4 = commit_change(
273 273 repo1.repo_name, filename='file1',
274 274 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
275 275 vcs_type=backend.alias, parent=commit3)
276 276 commit_change( # commit 5
277 277 repo1.repo_name, filename='file1',
278 278 content='line1\nline2\nline3\nline4\nline5\nline6\n',
279 279 message='commit6', vcs_type=backend.alias, parent=commit4)
280 280
281 281 response = self.app.get(
282 282 route_path('repo_compare',
283 283 repo_name=repo2.repo_name,
284 284 # parent of commit2, in target repo2
285 285 source_ref_type="rev", source_ref=commit1.raw_id,
286 286 target_ref_type="rev", target_ref=commit4.raw_id,
287 287 params=dict(merge='1', target_repo=repo1.repo_name),
288 288 ))
289 289 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
290 290 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
291 291
292 292 # files
293 293 compare_page = ComparePage(response)
294 294 compare_page.contains_change_summary(1, 3, 0)
295 295 compare_page.contains_commits([commit2, commit3, commit4])
296 296 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
297 297 compare_page.contains_file_links_and_anchors([('file1', anchor),])
298 298
299 299 @pytest.mark.xfail_backends("svn")
300 300 def test_compare_cherry_pick_commits_from_top(self, backend):
301 301 # repo1:
302 302 # commit0:
303 303 # commit1:
304 304 # repo1-fork- in which we will cherry pick bottom commits
305 305 # commit0:
306 306 # commit1:
307 307 # commit2:
308 308 # commit3: x
309 309 # commit4: x
310 310 # commit5: x
311 311
312 312 # make repo1, and commit1+commit2
313 313 repo1 = backend.create_repo()
314 314
315 315 # commit something !
316 316 commit0 = commit_change(
317 317 repo1.repo_name, filename='file1', content='line1\n',
318 318 message='commit1', vcs_type=backend.alias, parent=None,
319 319 newfile=True)
320 320 commit1 = commit_change(
321 321 repo1.repo_name, filename='file1', content='line1\nline2\n',
322 322 message='commit2', vcs_type=backend.alias, parent=commit0)
323 323
324 324 # fork this repo
325 325 backend.create_fork()
326 326
327 327 # now make commit3-6
328 328 commit2 = commit_change(
329 329 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
330 330 message='commit3', vcs_type=backend.alias, parent=commit1)
331 331 commit3 = commit_change(
332 332 repo1.repo_name, filename='file1',
333 333 content='line1\nline2\nline3\nline4\n', message='commit4',
334 334 vcs_type=backend.alias, parent=commit2)
335 335 commit4 = commit_change(
336 336 repo1.repo_name, filename='file1',
337 337 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
338 338 vcs_type=backend.alias, parent=commit3)
339 339 commit5 = commit_change(
340 340 repo1.repo_name, filename='file1',
341 341 content='line1\nline2\nline3\nline4\nline5\nline6\n',
342 342 message='commit6', vcs_type=backend.alias, parent=commit4)
343 343
344 344 response = self.app.get(
345 345 route_path('repo_compare',
346 346 repo_name=repo1.repo_name,
347 347 # parent of commit3, not in source repo2
348 348 source_ref_type="rev", source_ref=commit2.raw_id,
349 349 target_ref_type="rev", target_ref=commit5.raw_id,
350 350 params=dict(merge='1'),))
351 351
352 352 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
353 353 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
354 354
355 355 compare_page = ComparePage(response)
356 356 compare_page.contains_change_summary(1, 3, 0)
357 357 compare_page.contains_commits([commit3, commit4, commit5])
358 358
359 359 # files
360 360 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
361 361 compare_page.contains_file_links_and_anchors([('file1', anchor),])
362 362
363 363 @pytest.mark.xfail_backends("svn")
364 364 def test_compare_remote_branches(self, backend):
365 365 repo1 = backend.repo
366 366 repo2 = backend.create_fork()
367 367
368 368 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
369 369 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
370 370 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
371 371 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
372 372
373 373 response = self.app.get(
374 374 route_path('repo_compare',
375 375 repo_name=repo1.repo_name,
376 376 source_ref_type="rev", source_ref=commit_id1,
377 377 target_ref_type="rev", target_ref=commit_id2,
378 378 params=dict(merge='1', target_repo=repo2.repo_name),
379 379 ))
380 380
381 381 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
382 382 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
383 383
384 384 compare_page = ComparePage(response)
385 385
386 386 # outgoing commits between those commits
387 387 compare_page.contains_commits(
388 388 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
389 389
390 390 # files
391 391 compare_page.contains_file_links_and_anchors([
392 392 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
393 393 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
394 394 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
395 395 ])
396 396
397 397 @pytest.mark.xfail_backends("svn")
398 398 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
399 399 repo1 = backend.create_repo()
400 400 r1_name = repo1.repo_name
401 401
402 402 commit0 = commit_change(
403 403 repo=r1_name, filename='file1',
404 404 content='line1', message='commit1', vcs_type=backend.alias,
405 405 newfile=True)
406 406 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
407 407
408 408 # fork the repo1
409 409 repo2 = backend.create_fork()
410 410 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
411 411
412 412 self.r2_id = repo2.repo_id
413 413 r2_name = repo2.repo_name
414 414
415 415 commit1 = commit_change(
416 416 repo=r2_name, filename='file1-fork',
417 417 content='file1-line1-from-fork', message='commit1-fork',
418 418 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
419 419 newfile=True)
420 420
421 421 commit2 = commit_change(
422 422 repo=r2_name, filename='file2-fork',
423 423 content='file2-line1-from-fork', message='commit2-fork',
424 424 vcs_type=backend.alias, parent=commit1,
425 425 newfile=True)
426 426
427 427 commit_change( # commit 3
428 428 repo=r2_name, filename='file3-fork',
429 429 content='file3-line1-from-fork', message='commit3-fork',
430 430 vcs_type=backend.alias, parent=commit2, newfile=True)
431 431
432 432 # compare !
433 433 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
434 434 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
435 435
436 436 response = self.app.get(
437 437 route_path('repo_compare',
438 438 repo_name=r2_name,
439 439 source_ref_type="branch", source_ref=commit_id1,
440 440 target_ref_type="branch", target_ref=commit_id2,
441 441 params=dict(merge='1', target_repo=r1_name),
442 442 ))
443 443
444 444 response.mustcontain('%s@%s' % (r2_name, commit_id1))
445 445 response.mustcontain('%s@%s' % (r1_name, commit_id2))
446 446 response.mustcontain('No files')
447 447 response.mustcontain('No commits in this compare')
448 448
449 449 commit0 = commit_change(
450 450 repo=r1_name, filename='file2',
451 451 content='line1-added-after-fork', message='commit2-parent',
452 452 vcs_type=backend.alias, parent=None, newfile=True)
453 453
454 454 # compare !
455 455 response = self.app.get(
456 456 route_path('repo_compare',
457 457 repo_name=r2_name,
458 458 source_ref_type="branch", source_ref=commit_id1,
459 459 target_ref_type="branch", target_ref=commit_id2,
460 460 params=dict(merge='1', target_repo=r1_name),
461 461 ))
462 462
463 463 response.mustcontain('%s@%s' % (r2_name, commit_id1))
464 464 response.mustcontain('%s@%s' % (r1_name, commit_id2))
465 465
466 466 response.mustcontain("""commit2-parent""")
467 467 response.mustcontain("""line1-added-after-fork""")
468 468 compare_page = ComparePage(response)
469 469 compare_page.contains_change_summary(1, 1, 0)
470 470
471 471 @pytest.mark.xfail_backends("svn")
472 472 def test_compare_commits(self, backend, xhr_header):
473 473 commit0 = backend.repo.get_commit(commit_idx=0)
474 474 commit1 = backend.repo.get_commit(commit_idx=1)
475 475
476 476 response = self.app.get(
477 477 route_path('repo_compare',
478 478 repo_name=backend.repo_name,
479 479 source_ref_type="rev", source_ref=commit0.raw_id,
480 480 target_ref_type="rev", target_ref=commit1.raw_id,
481 481 params=dict(merge='1')
482 482 ),
483 483 extra_environ=xhr_header, )
484 484
485 485 # outgoing commits between those commits
486 486 compare_page = ComparePage(response)
487 487 compare_page.contains_commits(commits=[commit1], ancestors=[commit0])
488 488
489 489 def test_errors_when_comparing_unknown_source_repo(self, backend):
490 490 repo = backend.repo
491 491 badrepo = 'badrepo'
492 492
493 493 response = self.app.get(
494 494 route_path('repo_compare',
495 495 repo_name=badrepo,
496 496 source_ref_type="rev", source_ref='tip',
497 497 target_ref_type="rev", target_ref='tip',
498 498 params=dict(merge='1', target_repo=repo.repo_name)
499 499 ),
500 500 status=404)
501 501
502 502 def test_errors_when_comparing_unknown_target_repo(self, backend):
503 503 repo = backend.repo
504 504 badrepo = 'badrepo'
505 505
506 506 response = self.app.get(
507 507 route_path('repo_compare',
508 508 repo_name=repo.repo_name,
509 509 source_ref_type="rev", source_ref='tip',
510 510 target_ref_type="rev", target_ref='tip',
511 511 params=dict(merge='1', target_repo=badrepo),
512 512 ),
513 513 status=302)
514 514 redirected = response.follow()
515 515 redirected.mustcontain(
516 516 'Could not find the target repo: `{}`'.format(badrepo))
517 517
518 518 def test_compare_not_in_preview_mode(self, backend_stub):
519 519 commit0 = backend_stub.repo.get_commit(commit_idx=0)
520 520 commit1 = backend_stub.repo.get_commit(commit_idx=1)
521 521
522 522 response = self.app.get(
523 523 route_path('repo_compare',
524 524 repo_name=backend_stub.repo_name,
525 525 source_ref_type="rev", source_ref=commit0.raw_id,
526 526 target_ref_type="rev", target_ref=commit1.raw_id,
527 527 ))
528 528
529 529 # outgoing commits between those commits
530 530 compare_page = ComparePage(response)
531 531 compare_page.swap_is_visible()
532 532 compare_page.target_source_are_enabled()
533 533
534 534 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
535 535 orig = backend_hg.create_repo(number_of_commits=1)
536 536 fork = backend_hg.create_fork()
537 537
538 538 settings_util.create_repo_rhodecode_ui(
539 539 orig, 'extensions', value='', key='largefiles', active=False)
540 540 settings_util.create_repo_rhodecode_ui(
541 541 fork, 'extensions', value='', key='largefiles', active=True)
542 542
543 543 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
544 544 'MercurialRepository.compare')
545 545 with mock.patch(compare_module) as compare_mock:
546 546 compare_mock.side_effect = RepositoryRequirementError()
547 547
548 548 response = self.app.get(
549 549 route_path('repo_compare',
550 550 repo_name=orig.repo_name,
551 551 source_ref_type="rev", source_ref="tip",
552 552 target_ref_type="rev", target_ref="tip",
553 553 params=dict(merge='1', target_repo=fork.repo_name),
554 554 ),
555 555 status=302)
556 556
557 557 assert_session_flash(
558 558 response,
559 559 'Could not compare repos with different large file settings')
560 560
561 561
562 562 @pytest.mark.usefixtures("autologin_user")
563 563 class TestCompareControllerSvn(object):
564 564
565 565 def test_supports_references_with_path(self, app, backend_svn):
566 566 repo = backend_svn['svn-simple-layout']
567 567 commit_id = repo.get_commit(commit_idx=-1).raw_id
568 568 response = app.get(
569 569 route_path('repo_compare',
570 570 repo_name=repo.repo_name,
571 571 source_ref_type="tag",
572 572 source_ref="%s@%s" % ('tags/v0.1', commit_id),
573 573 target_ref_type="tag",
574 574 target_ref="%s@%s" % ('tags/v0.2', commit_id),
575 575 params=dict(merge='1'),
576 576 ),
577 577 status=200)
578 578
579 579 # Expecting no commits, since both paths are at the same revision
580 580 response.mustcontain('No commits in this compare')
581 581
582 582 # Should find only one file changed when comparing those two tags
583 583 response.mustcontain('example.py')
584 584 compare_page = ComparePage(response)
585 585 compare_page.contains_change_summary(1, 5, 1)
586 586
587 587 def test_shows_commits_if_different_ids(self, app, backend_svn):
588 588 repo = backend_svn['svn-simple-layout']
589 589 source_id = repo.get_commit(commit_idx=-6).raw_id
590 590 target_id = repo.get_commit(commit_idx=-1).raw_id
591 591 response = app.get(
592 592 route_path('repo_compare',
593 593 repo_name=repo.repo_name,
594 594 source_ref_type="tag",
595 595 source_ref="%s@%s" % ('tags/v0.1', source_id),
596 596 target_ref_type="tag",
597 597 target_ref="%s@%s" % ('tags/v0.2', target_id),
598 598 params=dict(merge='1')
599 599 ),
600 600 status=200)
601 601
602 602 # It should show commits
603 603 assert 'No commits in this compare' not in response.body
604 604
605 605 # Should find only one file changed when comparing those two tags
606 606 response.mustcontain('example.py')
607 607 compare_page = ComparePage(response)
608 608 compare_page.contains_change_summary(1, 5, 1)
609 609
610 610
611 611 class ComparePage(AssertResponse):
612 612 """
613 613 Abstracts the page template from the tests
614 614 """
615 615
616 616 def contains_file_links_and_anchors(self, files):
617 617 doc = lxml.html.fromstring(self.response.body)
618 618 for filename, file_id in files:
619 619 self.contains_one_anchor(file_id)
620 620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
621 621 assert len(diffblock) == 2
622 622 assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
623 623
624 624 def contains_change_summary(self, files_changed, inserted, deleted):
625 625 template = (
626 626 "{files_changed} file{plural} changed: "
627 627 "{inserted} inserted, {deleted} deleted")
628 628 self.response.mustcontain(template.format(
629 629 files_changed=files_changed,
630 630 plural="s" if files_changed > 1 else "",
631 631 inserted=inserted,
632 632 deleted=deleted))
633 633
634 634 def contains_commits(self, commits, ancestors=None):
635 635 response = self.response
636 636
637 637 for commit in commits:
638 638 # Expecting to see the commit message in an element which
639 639 # has the ID "c-{commit.raw_id}"
640 640 self.element_contains('#c-' + commit.raw_id, commit.message)
641 641 self.contains_one_link(
642 642 'r%s:%s' % (commit.idx, commit.short_id),
643 643 self._commit_url(commit))
644 644 if ancestors:
645 645 response.mustcontain('Ancestor')
646 646 for ancestor in ancestors:
647 647 self.contains_one_link(
648 648 ancestor.short_id, self._commit_url(ancestor))
649 649
650 650 def _commit_url(self, commit):
651 651 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
652 652
653 653 def swap_is_hidden(self):
654 654 assert '<a id="btn-swap"' not in self.response.text
655 655
656 656 def swap_is_visible(self):
657 657 assert '<a id="btn-swap"' in self.response.text
658 658
659 659 def target_source_are_disabled(self):
660 660 response = self.response
661 661 response.mustcontain("var enable_fields = false;")
662 662 response.mustcontain('.select2("enable", enable_fields)')
663 663
664 664 def target_source_are_enabled(self):
665 665 response = self.response
666 666 response.mustcontain("var enable_fields = true;")
@@ -1,183 +1,259 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs.backends.base import EmptyCommit
24 25 from rhodecode.tests.fixture import Fixture
25 26 from rhodecode.tests.utils import commit_change
26 27
27 28 fixture = Fixture()
28 29
29 30
30 31 def route_path(name, params=None, **kwargs):
31 32 import urllib
32 33
33 34 base_url = {
34 35 'repo_compare_select': '/{repo_name}/compare',
35 36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 37 }[name].format(**kwargs)
37 38
38 39 if params:
39 40 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 41 return base_url
41 42
42 43
43 44 @pytest.mark.usefixtures("autologin_user", "app")
44 45 class TestSideBySideDiff(object):
45 46
46 def test_diff_side_by_side(self, app, backend, backend_stub):
47 f_path = 'test_sidebyside_file.py'
48 commit1_content = 'content-25d7e49c18b159446c\n'
49 commit2_content = 'content-603d6c72c46d953420\n'
50 repo = backend.create_repo()
51
52 commit1 = commit_change(
53 repo.repo_name, filename=f_path, content=commit1_content,
54 message='A', vcs_type=backend.alias, parent=None, newfile=True)
55
56 commit2 = commit_change(
57 repo.repo_name, filename=f_path, content=commit2_content,
58 message='B, child of A', vcs_type=backend.alias, parent=commit1)
59
60 response = self.app.get(route_path(
61 'repo_compare',
62 repo_name=repo.repo_name,
63 source_ref_type='rev',
64 source_ref=commit1.raw_id,
65 target_ref_type='rev',
66 target_ref=commit2.raw_id,
67 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
68 ))
69
70 response.mustcontain('Expand 1 commit')
71 response.mustcontain('1 file changed')
72
73 response.mustcontain(
74 'r%s:%s...r%s:%s' % (
75 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
76
77 response.mustcontain('<strong>{}</strong>'.format(f_path))
78
79 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
80 commits = [
81 {'message': 'First commit'},
82 {'message': 'Commit with binary',
83 'added': [nodes.FileNode('file.empty', content='')]},
84 ]
85 f_path = 'file.empty'
86 repo = backend.create_repo(commits=commits)
87 commit1 = repo.get_commit(commit_idx=0)
88 commit2 = repo.get_commit(commit_idx=1)
89
90 response = self.app.get(route_path(
91 'repo_compare',
92 repo_name=repo.repo_name,
93 source_ref_type='rev',
94 source_ref=commit1.raw_id,
95 target_ref_type='rev',
96 target_ref=commit2.raw_id,
97 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
98 ))
99
100 response.mustcontain('Expand 1 commit')
101 response.mustcontain('1 file changed')
102
103 response.mustcontain(
104 'r%s:%s...r%s:%s' % (
105 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
106
107 response.mustcontain('<strong>{}</strong>'.format(f_path))
108
109 def test_diff_sidebyside_two_commits(self, app, backend):
47 def test_diff_sidebyside_single_commit(self, app, backend):
110 48 commit_id_range = {
111 49 'hg': {
112 50 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
113 51 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
114 52 'changes': '21 files changed: 943 inserted, 288 deleted'
115 53 },
116 54 'git': {
117 55 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
118 56 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
119 57 'changes': '21 files changed: 943 inserted, 288 deleted'
120 58 },
121 59
122 60 'svn': {
123 61 'commits': ['336',
124 62 '337'],
125 63 'changes': '21 files changed: 943 inserted, 288 deleted'
126 64 },
127 65 }
128 66
129 67 commit_info = commit_id_range[backend.alias]
130 68 commit2, commit1 = commit_info['commits']
131 69 file_changes = commit_info['changes']
132 70
133 71 response = self.app.get(route_path(
134 72 'repo_compare',
135 73 repo_name=backend.repo_name,
136 74 source_ref_type='rev',
137 75 source_ref=commit2,
138 76 target_repo=backend.repo_name,
139 77 target_ref_type='rev',
140 78 target_ref=commit1,
141 79 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
142 80 ))
143 81
82 response.mustcontain(file_changes)
144 83 response.mustcontain('Expand 1 commit')
145 response.mustcontain(file_changes)
146 84
147 def test_diff_sidebyside_two_commits_single_file(self, app, backend):
85 def test_diff_sidebyside_two_commits(self, app, backend):
148 86 commit_id_range = {
149 87 'hg': {
150 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
88 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
151 89 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
152 'changes': '1 file changed: 1 inserted, 1 deleted'
90 'changes': '32 files changed: 1165 inserted, 308 deleted'
153 91 },
154 92 'git': {
155 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
93 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
156 94 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
157 'changes': '1 file changed: 1 inserted, 1 deleted'
95 'changes': '32 files changed: 1165 inserted, 308 deleted'
158 96 },
159 97
160 98 'svn': {
161 'commits': ['336',
99 'commits': ['335',
162 100 '337'],
163 'changes': '1 file changed: 1 inserted, 1 deleted'
101 'changes': '32 files changed: 1179 inserted, 310 deleted'
102 },
103 }
104
105 commit_info = commit_id_range[backend.alias]
106 commit2, commit1 = commit_info['commits']
107 file_changes = commit_info['changes']
108
109 response = self.app.get(route_path(
110 'repo_compare',
111 repo_name=backend.repo_name,
112 source_ref_type='rev',
113 source_ref=commit2,
114 target_repo=backend.repo_name,
115 target_ref_type='rev',
116 target_ref=commit1,
117 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
118 ))
119
120 response.mustcontain(file_changes)
121 response.mustcontain('Expand 2 commits')
122
123 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
124 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
125 f_path = 'test_sidebyside_file.py'
126 commit1_content = 'content-25d7e49c18b159446c\n'
127 commit2_content = 'content-603d6c72c46d953420\n'
128 repo = backend.create_repo()
129
130 commit1 = commit_change(
131 repo.repo_name, filename=f_path, content=commit1_content,
132 message='A', vcs_type=backend.alias, parent=None, newfile=True)
133
134 commit2 = commit_change(
135 repo.repo_name, filename=f_path, content=commit2_content,
136 message='B, child of A', vcs_type=backend.alias, parent=commit1)
137
138 response = self.app.get(route_path(
139 'repo_compare',
140 repo_name=repo.repo_name,
141 source_ref_type='rev',
142 source_ref=EmptyCommit().raw_id,
143 target_ref_type='rev',
144 target_ref=commit2.raw_id,
145 params=dict(diffmode='sidebyside')
146 ))
147
148 response.mustcontain('Expand 2 commits')
149 response.mustcontain('123 file changed')
150
151 response.mustcontain(
152 'r%s:%s...r%s:%s' % (
153 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
154
155 response.mustcontain('<strong>{}</strong>'.format(f_path))
156
157 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
158 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
159 f_path = 'test_sidebyside_file.py'
160 commit1_content = 'content-25d7e49c18b159446c\n'
161 commit2_content = 'content-603d6c72c46d953420\n'
162 repo = backend.create_repo()
163
164 commit1 = commit_change(
165 repo.repo_name, filename=f_path, content=commit1_content,
166 message='A', vcs_type=backend.alias, parent=None, newfile=True)
167
168 commit2 = commit_change(
169 repo.repo_name, filename=f_path, content=commit2_content,
170 message='B, child of A', vcs_type=backend.alias, parent=commit1)
171
172 response = self.app.get(route_path(
173 'repo_compare',
174 repo_name=repo.repo_name,
175 source_ref_type='rev',
176 source_ref=EmptyCommit().raw_id,
177 target_ref_type='rev',
178 target_ref=commit2.raw_id,
179 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
180 ))
181
182 response.mustcontain('Expand 2 commits')
183 response.mustcontain('1 file changed')
184
185 response.mustcontain(
186 'r%s:%s...r%s:%s' % (
187 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
188
189 response.mustcontain('<strong>{}</strong>'.format(f_path))
190
191 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
192 commits = [
193 {'message': 'First commit'},
194 {'message': 'Second commit'},
195 {'message': 'Commit with binary',
196 'added': [nodes.FileNode('file.empty', content='')]},
197 ]
198 f_path = 'file.empty'
199 repo = backend.create_repo(commits=commits)
200 commit1 = repo.get_commit(commit_idx=0)
201 commit2 = repo.get_commit(commit_idx=1)
202 commit3 = repo.get_commit(commit_idx=2)
203
204 response = self.app.get(route_path(
205 'repo_compare',
206 repo_name=repo.repo_name,
207 source_ref_type='rev',
208 source_ref=commit1.raw_id,
209 target_ref_type='rev',
210 target_ref=commit3.raw_id,
211 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
212 ))
213
214 response.mustcontain('Expand 2 commits')
215 response.mustcontain('1 file changed')
216
217 response.mustcontain(
218 'r%s:%s...r%s:%s' % (
219 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
220
221 response.mustcontain('<strong>{}</strong>'.format(f_path))
222
223 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
224 commit_id_range = {
225 'hg': {
226 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
227 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
228 'changes': '1 file changed: 3 inserted, 3 deleted'
229 },
230 'git': {
231 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
232 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
233 'changes': '1 file changed: 3 inserted, 3 deleted'
234 },
235
236 'svn': {
237 'commits': ['335',
238 '337'],
239 'changes': '1 file changed: 3 inserted, 3 deleted'
164 240 },
165 241 }
166 242 f_path = 'docs/conf.py'
167 243
168 244 commit_info = commit_id_range[backend.alias]
169 245 commit2, commit1 = commit_info['commits']
170 246 file_changes = commit_info['changes']
171 247
172 248 response = self.app.get(route_path(
173 249 'repo_compare',
174 250 repo_name=backend.repo_name,
175 251 source_ref_type='rev',
176 252 source_ref=commit2,
177 253 target_ref_type='rev',
178 254 target_ref=commit1,
179 255 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
180 256 ))
181 257
182 response.mustcontain('Expand 1 commit')
258 response.mustcontain('Expand 2 commits')
183 259 response.mustcontain(file_changes)
@@ -1,317 +1,311 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 25 from pyramid.view import view_config
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.utils import safe_str
35 35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 37 from rhodecode.lib.vcs.exceptions import (
38 38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 39 NodeDoesNotExistError)
40 40 from rhodecode.model.db import Repository, ChangesetStatus
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoCompareView(RepoAppView):
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context(include_app_defaults=True)
48 48 c.rhodecode_repo = self.rhodecode_vcs_repo
49 49 return c
50 50
51 51 def _get_commit_or_redirect(
52 52 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 53 """
54 54 This is a safe way to get a commit. If an error occurs it
55 55 redirects to a commit with a proper message. If partial is set
56 56 then it does not do redirect raise and throws an exception instead.
57 57 """
58 58 _ = self.request.translate
59 59 try:
60 60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return repo.scm_instance().EMPTY_COMMIT
64 64 h.flash(h.literal(_('There are no commits yet')),
65 65 category='warning')
66 66 if not partial:
67 67 raise HTTPFound(
68 68 h.route_path('repo_summary', repo_name=repo.repo_name))
69 69 raise HTTPBadRequest()
70 70
71 71 except RepositoryError as e:
72 72 log.exception(safe_str(e))
73 73 h.flash(safe_str(h.escape(e)), category='warning')
74 74 if not partial:
75 75 raise HTTPFound(
76 76 h.route_path('repo_summary', repo_name=repo.repo_name))
77 77 raise HTTPBadRequest()
78 78
79 79 @LoginRequired()
80 80 @HasRepoPermissionAnyDecorator(
81 81 'repository.read', 'repository.write', 'repository.admin')
82 82 @view_config(
83 83 route_name='repo_compare_select', request_method='GET',
84 84 renderer='rhodecode:templates/compare/compare_diff.mako')
85 85 def compare_select(self):
86 86 _ = self.request.translate
87 87 c = self.load_default_context()
88 88
89 89 source_repo = self.db_repo_name
90 90 target_repo = self.request.GET.get('target_repo', source_repo)
91 91 c.source_repo = Repository.get_by_repo_name(source_repo)
92 92 c.target_repo = Repository.get_by_repo_name(target_repo)
93 93
94 94 if c.source_repo is None or c.target_repo is None:
95 95 raise HTTPNotFound()
96 96
97 97 c.compare_home = True
98 98 c.commit_ranges = []
99 99 c.collapse_all_commits = False
100 100 c.diffset = None
101 101 c.limited_diff = False
102 102 c.source_ref = c.target_ref = _('Select commit')
103 103 c.source_ref_type = ""
104 104 c.target_ref_type = ""
105 105 c.commit_statuses = ChangesetStatus.STATUSES
106 106 c.preview_mode = False
107 107 c.file_path = None
108 108
109 109 return self._get_template_context(c)
110 110
111 111 @LoginRequired()
112 112 @HasRepoPermissionAnyDecorator(
113 113 'repository.read', 'repository.write', 'repository.admin')
114 114 @view_config(
115 115 route_name='repo_compare', request_method='GET',
116 116 renderer=None)
117 117 def compare(self):
118 118 _ = self.request.translate
119 119 c = self.load_default_context()
120 120
121 121 source_ref_type = self.request.matchdict['source_ref_type']
122 122 source_ref = self.request.matchdict['source_ref']
123 123 target_ref_type = self.request.matchdict['target_ref_type']
124 124 target_ref = self.request.matchdict['target_ref']
125 125
126 126 # source_ref will be evaluated in source_repo
127 127 source_repo_name = self.db_repo_name
128 128 source_path, source_id = parse_path_ref(source_ref)
129 129
130 130 # target_ref will be evaluated in target_repo
131 131 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
132 132 target_path, target_id = parse_path_ref(
133 133 target_ref, default_path=self.request.GET.get('f_path', ''))
134 134
135 135 # if merge is True
136 136 # Show what changes since the shared ancestor commit of target/source
137 137 # the source would get if it was merged with target. Only commits
138 138 # which are in target but not in source will be shown.
139 139 merge = str2bool(self.request.GET.get('merge'))
140 140 # if merge is False
141 141 # Show a raw diff of source/target refs even if no ancestor exists
142 142
143 143 # c.fulldiff disables cut_off_limit
144 144 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
145 145
146 146 # fetch global flags of ignore ws or context lines
147 147 diff_context = diffs.get_diff_context(self.request)
148 148 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
149 149
150 150 c.file_path = target_path
151 151 c.commit_statuses = ChangesetStatus.STATUSES
152 152
153 153 # if partial, returns just compare_commits.html (commits log)
154 154 partial = self.request.is_xhr
155 155
156 156 # swap url for compare_diff page
157 157 c.swap_url = h.route_path(
158 158 'repo_compare',
159 159 repo_name=target_repo_name,
160 160 source_ref_type=target_ref_type,
161 161 source_ref=target_ref,
162 162 target_repo=source_repo_name,
163 163 target_ref_type=source_ref_type,
164 164 target_ref=source_ref,
165 165 _query=dict(merge=merge and '1' or '', f_path=target_path))
166 166
167 167 source_repo = Repository.get_by_repo_name(source_repo_name)
168 168 target_repo = Repository.get_by_repo_name(target_repo_name)
169 169
170 170 if source_repo is None:
171 171 log.error('Could not find the source repo: {}'
172 172 .format(source_repo_name))
173 173 h.flash(_('Could not find the source repo: `{}`')
174 174 .format(h.escape(source_repo_name)), category='error')
175 175 raise HTTPFound(
176 176 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
177 177
178 178 if target_repo is None:
179 179 log.error('Could not find the target repo: {}'
180 180 .format(source_repo_name))
181 181 h.flash(_('Could not find the target repo: `{}`')
182 182 .format(h.escape(target_repo_name)), category='error')
183 183 raise HTTPFound(
184 184 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
185 185
186 186 source_scm = source_repo.scm_instance()
187 187 target_scm = target_repo.scm_instance()
188 188
189 189 source_alias = source_scm.alias
190 190 target_alias = target_scm.alias
191 191 if source_alias != target_alias:
192 192 msg = _('The comparison of two different kinds of remote repos '
193 193 'is not available')
194 194 log.error(msg)
195 195 h.flash(msg, category='error')
196 196 raise HTTPFound(
197 197 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
198 198
199 199 source_commit = self._get_commit_or_redirect(
200 200 ref=source_id, ref_type=source_ref_type, repo=source_repo,
201 201 partial=partial)
202 202 target_commit = self._get_commit_or_redirect(
203 203 ref=target_id, ref_type=target_ref_type, repo=target_repo,
204 204 partial=partial)
205 205
206 206 c.compare_home = False
207 207 c.source_repo = source_repo
208 208 c.target_repo = target_repo
209 209 c.source_ref = source_ref
210 210 c.target_ref = target_ref
211 211 c.source_ref_type = source_ref_type
212 212 c.target_ref_type = target_ref_type
213 213
214 214 pre_load = ["author", "branch", "date", "message"]
215 215 c.ancestor = None
216 216
217 if c.file_path:
218 if source_commit == target_commit:
219 c.commit_ranges = []
220 else:
221 c.commit_ranges = [source_commit, target_commit]
222 else:
223 try:
224 c.commit_ranges = source_scm.compare(
225 source_commit.raw_id, target_commit.raw_id,
226 target_scm, merge, pre_load=pre_load)
227 if merge:
228 c.ancestor = source_scm.get_common_ancestor(
229 source_commit.raw_id, target_commit.raw_id, target_scm)
230 except RepositoryRequirementError:
231 msg = _('Could not compare repos with different '
232 'large file settings')
233 log.error(msg)
234 if partial:
235 return Response(msg)
236 h.flash(msg, category='error')
237 raise HTTPFound(
238 h.route_path('repo_compare_select',
239 repo_name=self.db_repo_name))
217 try:
218 c.commit_ranges = source_scm.compare(
219 source_commit.raw_id, target_commit.raw_id,
220 target_scm, merge, pre_load=pre_load) or []
221 if merge:
222 c.ancestor = source_scm.get_common_ancestor(
223 source_commit.raw_id, target_commit.raw_id, target_scm)
224 except RepositoryRequirementError:
225 msg = _('Could not compare repos with different '
226 'large file settings')
227 log.error(msg)
228 if partial:
229 return Response(msg)
230 h.flash(msg, category='error')
231 raise HTTPFound(
232 h.route_path('repo_compare_select',
233 repo_name=self.db_repo_name))
240 234
241 235 c.statuses = self.db_repo.statuses(
242 236 [x.raw_id for x in c.commit_ranges])
243 237
244 238 # auto collapse if we have more than limit
245 239 collapse_limit = diffs.DiffProcessor._collapse_commits_over
246 240 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
247 241
248 242 if partial: # for PR ajax commits loader
249 243 if not c.ancestor:
250 244 return Response('') # cannot merge if there is no ancestor
251 245
252 246 html = render(
253 247 'rhodecode:templates/compare/compare_commits.mako',
254 248 self._get_template_context(c), self.request)
255 249 return Response(html)
256 250
257 251 if c.ancestor:
258 252 # case we want a simple diff without incoming commits,
259 253 # previewing what will be merged.
260 254 # Make the diff on target repo (which is known to have target_ref)
261 255 log.debug('Using ancestor %s as source_ref instead of %s',
262 256 c.ancestor, source_ref)
263 257 source_repo = target_repo
264 258 source_commit = target_repo.get_commit(commit_id=c.ancestor)
265 259
266 260 # diff_limit will cut off the whole diff if the limit is applied
267 261 # otherwise it will just hide the big files from the front-end
268 262 diff_limit = c.visual.cut_off_limit_diff
269 263 file_limit = c.visual.cut_off_limit_file
270 264
271 265 log.debug('calculating diff between '
272 266 'source_ref:%s and target_ref:%s for repo `%s`',
273 267 source_commit, target_commit,
274 268 safe_unicode(source_repo.scm_instance().path))
275 269
276 270 if source_commit.repository != target_commit.repository:
277 271 msg = _(
278 272 "Repositories unrelated. "
279 273 "Cannot compare commit %(commit1)s from repository %(repo1)s "
280 274 "with commit %(commit2)s from repository %(repo2)s.") % {
281 275 'commit1': h.show_id(source_commit),
282 276 'repo1': source_repo.repo_name,
283 277 'commit2': h.show_id(target_commit),
284 278 'repo2': target_repo.repo_name,
285 279 }
286 280 h.flash(msg, category='error')
287 281 raise HTTPFound(
288 282 h.route_path('repo_compare_select',
289 283 repo_name=self.db_repo_name))
290 284
291 285 txt_diff = source_repo.scm_instance().get_diff(
292 286 commit1=source_commit, commit2=target_commit,
293 287 path=target_path, path1=source_path,
294 288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
295 289
296 290 diff_processor = diffs.DiffProcessor(
297 291 txt_diff, format='newdiff', diff_limit=diff_limit,
298 292 file_limit=file_limit, show_full_diff=c.fulldiff)
299 293 _parsed = diff_processor.prepare()
300 294
301 295 diffset = codeblocks.DiffSet(
302 296 repo_name=source_repo.repo_name,
303 297 source_node_getter=codeblocks.diffset_node_getter(source_commit),
304 298 target_repo_name=self.db_repo_name,
305 299 target_node_getter=codeblocks.diffset_node_getter(target_commit),
306 300 )
307 301 c.diffset = self.path_filter.render_patchset_filtered(
308 302 diffset, _parsed, source_ref, target_ref)
309 303
310 304 c.preview_mode = merge
311 305 c.source_commit = source_commit
312 306 c.target_commit = target_commit
313 307
314 308 html = render(
315 309 'rhodecode:templates/compare/compare_diff.mako',
316 310 self._get_template_context(c), self.request)
317 311 return Response(html) No newline at end of file
@@ -1,1860 +1,1861 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Base module for all VCS systems
23 23 """
24 24 import os
25 25 import re
26 26 import time
27 27 import shutil
28 28 import datetime
29 29 import fnmatch
30 30 import itertools
31 31 import logging
32 32 import collections
33 33 import warnings
34 34
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36 from zope.cachedescriptors.property import CachedProperty
37 37
38 38 from pyramid import compat
39 39
40 40 from rhodecode.translation import lazy_ugettext
41 41 from rhodecode.lib.utils2 import safe_str, safe_unicode
42 42 from rhodecode.lib.vcs import connection
43 43 from rhodecode.lib.vcs.utils import author_name, author_email
44 44 from rhodecode.lib.vcs.conf import settings
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
47 47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
48 48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
49 49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
50 50 RepositoryError)
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 FILEMODE_DEFAULT = 0o100644
57 57 FILEMODE_EXECUTABLE = 0o100755
58 EMPTY_COMMIT_ID = '0' * 40
58 59
59 60 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 61
61 62
62 63 class MergeFailureReason(object):
63 64 """
64 65 Enumeration with all the reasons why the server side merge could fail.
65 66
66 67 DO NOT change the number of the reasons, as they may be stored in the
67 68 database.
68 69
69 70 Changing the name of a reason is acceptable and encouraged to deprecate old
70 71 reasons.
71 72 """
72 73
73 74 # Everything went well.
74 75 NONE = 0
75 76
76 77 # An unexpected exception was raised. Check the logs for more details.
77 78 UNKNOWN = 1
78 79
79 80 # The merge was not successful, there are conflicts.
80 81 MERGE_FAILED = 2
81 82
82 83 # The merge succeeded but we could not push it to the target repository.
83 84 PUSH_FAILED = 3
84 85
85 86 # The specified target is not a head in the target repository.
86 87 TARGET_IS_NOT_HEAD = 4
87 88
88 89 # The source repository contains more branches than the target. Pushing
89 90 # the merge will create additional branches in the target.
90 91 HG_SOURCE_HAS_MORE_BRANCHES = 5
91 92
92 93 # The target reference has multiple heads. That does not allow to correctly
93 94 # identify the target location. This could only happen for mercurial
94 95 # branches.
95 96 HG_TARGET_HAS_MULTIPLE_HEADS = 6
96 97
97 98 # The target repository is locked
98 99 TARGET_IS_LOCKED = 7
99 100
100 101 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
101 102 # A involved commit could not be found.
102 103 _DEPRECATED_MISSING_COMMIT = 8
103 104
104 105 # The target repo reference is missing.
105 106 MISSING_TARGET_REF = 9
106 107
107 108 # The source repo reference is missing.
108 109 MISSING_SOURCE_REF = 10
109 110
110 111 # The merge was not successful, there are conflicts related to sub
111 112 # repositories.
112 113 SUBREPO_MERGE_FAILED = 11
113 114
114 115
115 116 class UpdateFailureReason(object):
116 117 """
117 118 Enumeration with all the reasons why the pull request update could fail.
118 119
119 120 DO NOT change the number of the reasons, as they may be stored in the
120 121 database.
121 122
122 123 Changing the name of a reason is acceptable and encouraged to deprecate old
123 124 reasons.
124 125 """
125 126
126 127 # Everything went well.
127 128 NONE = 0
128 129
129 130 # An unexpected exception was raised. Check the logs for more details.
130 131 UNKNOWN = 1
131 132
132 133 # The pull request is up to date.
133 134 NO_CHANGE = 2
134 135
135 136 # The pull request has a reference type that is not supported for update.
136 137 WRONG_REF_TYPE = 3
137 138
138 139 # Update failed because the target reference is missing.
139 140 MISSING_TARGET_REF = 4
140 141
141 142 # Update failed because the source reference is missing.
142 143 MISSING_SOURCE_REF = 5
143 144
144 145
145 146 class MergeResponse(object):
146 147
147 148 # uses .format(**metadata) for variables
148 149 MERGE_STATUS_MESSAGES = {
149 150 MergeFailureReason.NONE: lazy_ugettext(
150 151 u'This pull request can be automatically merged.'),
151 152 MergeFailureReason.UNKNOWN: lazy_ugettext(
152 153 u'This pull request cannot be merged because of an unhandled exception. '
153 154 u'{exception}'),
154 155 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
155 156 u'This pull request cannot be merged because of merge conflicts.'),
156 157 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
157 158 u'This pull request could not be merged because push to '
158 159 u'target:`{target}@{merge_commit}` failed.'),
159 160 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
160 161 u'This pull request cannot be merged because the target '
161 162 u'`{target_ref.name}` is not a head.'),
162 163 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
163 164 u'This pull request cannot be merged because the source contains '
164 165 u'more branches than the target.'),
165 166 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
166 167 u'This pull request cannot be merged because the target `{target_ref.name}` '
167 168 u'has multiple heads: `{heads}`.'),
168 169 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
169 170 u'This pull request cannot be merged because the target repository is '
170 171 u'locked by {locked_by}.'),
171 172
172 173 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
173 174 u'This pull request cannot be merged because the target '
174 175 u'reference `{target_ref.name}` is missing.'),
175 176 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
176 177 u'This pull request cannot be merged because the source '
177 178 u'reference `{source_ref.name}` is missing.'),
178 179 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
179 180 u'This pull request cannot be merged because of conflicts related '
180 181 u'to sub repositories.'),
181 182
182 183 # Deprecations
183 184 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
184 185 u'This pull request cannot be merged because the target or the '
185 186 u'source reference is missing.'),
186 187
187 188 }
188 189
189 190 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
190 191 self.possible = possible
191 192 self.executed = executed
192 193 self.merge_ref = merge_ref
193 194 self.failure_reason = failure_reason
194 195 self.metadata = metadata or {}
195 196
196 197 def __repr__(self):
197 198 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
198 199
199 200 def __eq__(self, other):
200 201 same_instance = isinstance(other, self.__class__)
201 202 return same_instance \
202 203 and self.possible == other.possible \
203 204 and self.executed == other.executed \
204 205 and self.failure_reason == other.failure_reason
205 206
206 207 @property
207 208 def label(self):
208 209 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
209 210 not k.startswith('_'))
210 211 return label_dict.get(self.failure_reason)
211 212
212 213 @property
213 214 def merge_status_message(self):
214 215 """
215 216 Return a human friendly error message for the given merge status code.
216 217 """
217 218 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
218 219 try:
219 220 return msg.format(**self.metadata)
220 221 except Exception:
221 222 log.exception('Failed to format %s message', self)
222 223 return msg
223 224
224 225 def asdict(self):
225 226 data = {}
226 227 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
227 228 'merge_status_message']:
228 229 data[k] = getattr(self, k)
229 230 return data
230 231
231 232
232 233 class BaseRepository(object):
233 234 """
234 235 Base Repository for final backends
235 236
236 237 .. attribute:: DEFAULT_BRANCH_NAME
237 238
238 239 name of default branch (i.e. "trunk" for svn, "master" for git etc.
239 240
240 241 .. attribute:: commit_ids
241 242
242 243 list of all available commit ids, in ascending order
243 244
244 245 .. attribute:: path
245 246
246 247 absolute path to the repository
247 248
248 249 .. attribute:: bookmarks
249 250
250 251 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
251 252 there are no bookmarks or the backend implementation does not support
252 253 bookmarks.
253 254
254 255 .. attribute:: tags
255 256
256 257 Mapping from name to :term:`Commit ID` of the tag.
257 258
258 259 """
259 260
260 261 DEFAULT_BRANCH_NAME = None
261 262 DEFAULT_CONTACT = u"Unknown"
262 263 DEFAULT_DESCRIPTION = u"unknown"
263 264 EMPTY_COMMIT_ID = '0' * 40
264 265
265 266 path = None
266 267 _commit_ids_ver = 0
267 268
268 269 def __init__(self, repo_path, config=None, create=False, **kwargs):
269 270 """
270 271 Initializes repository. Raises RepositoryError if repository could
271 272 not be find at the given ``repo_path`` or directory at ``repo_path``
272 273 exists and ``create`` is set to True.
273 274
274 275 :param repo_path: local path of the repository
275 276 :param config: repository configuration
276 277 :param create=False: if set to True, would try to create repository.
277 278 :param src_url=None: if set, should be proper url from which repository
278 279 would be cloned; requires ``create`` parameter to be set to True -
279 280 raises RepositoryError if src_url is set and create evaluates to
280 281 False
281 282 """
282 283 raise NotImplementedError
283 284
284 285 def __repr__(self):
285 286 return '<%s at %s>' % (self.__class__.__name__, self.path)
286 287
287 288 def __len__(self):
288 289 return self.count()
289 290
290 291 def __eq__(self, other):
291 292 same_instance = isinstance(other, self.__class__)
292 293 return same_instance and other.path == self.path
293 294
294 295 def __ne__(self, other):
295 296 return not self.__eq__(other)
296 297
297 298 def get_create_shadow_cache_pr_path(self, db_repo):
298 299 path = db_repo.cached_diffs_dir
299 300 if not os.path.exists(path):
300 301 os.makedirs(path, 0o755)
301 302 return path
302 303
303 304 @classmethod
304 305 def get_default_config(cls, default=None):
305 306 config = Config()
306 307 if default and isinstance(default, list):
307 308 for section, key, val in default:
308 309 config.set(section, key, val)
309 310 return config
310 311
311 312 @LazyProperty
312 313 def _remote(self):
313 314 raise NotImplementedError
314 315
315 316 def _heads(self, branch=None):
316 317 return []
317 318
318 319 @LazyProperty
319 320 def EMPTY_COMMIT(self):
320 321 return EmptyCommit(self.EMPTY_COMMIT_ID)
321 322
322 323 @LazyProperty
323 324 def alias(self):
324 325 for k, v in settings.BACKENDS.items():
325 326 if v.split('.')[-1] == str(self.__class__.__name__):
326 327 return k
327 328
328 329 @LazyProperty
329 330 def name(self):
330 331 return safe_unicode(os.path.basename(self.path))
331 332
332 333 @LazyProperty
333 334 def description(self):
334 335 raise NotImplementedError
335 336
336 337 def refs(self):
337 338 """
338 339 returns a `dict` with branches, bookmarks, tags, and closed_branches
339 340 for this repository
340 341 """
341 342 return dict(
342 343 branches=self.branches,
343 344 branches_closed=self.branches_closed,
344 345 tags=self.tags,
345 346 bookmarks=self.bookmarks
346 347 )
347 348
348 349 @LazyProperty
349 350 def branches(self):
350 351 """
351 352 A `dict` which maps branch names to commit ids.
352 353 """
353 354 raise NotImplementedError
354 355
355 356 @LazyProperty
356 357 def branches_closed(self):
357 358 """
358 359 A `dict` which maps tags names to commit ids.
359 360 """
360 361 raise NotImplementedError
361 362
362 363 @LazyProperty
363 364 def bookmarks(self):
364 365 """
365 366 A `dict` which maps tags names to commit ids.
366 367 """
367 368 raise NotImplementedError
368 369
369 370 @LazyProperty
370 371 def tags(self):
371 372 """
372 373 A `dict` which maps tags names to commit ids.
373 374 """
374 375 raise NotImplementedError
375 376
376 377 @LazyProperty
377 378 def size(self):
378 379 """
379 380 Returns combined size in bytes for all repository files
380 381 """
381 382 tip = self.get_commit()
382 383 return tip.size
383 384
384 385 def size_at_commit(self, commit_id):
385 386 commit = self.get_commit(commit_id)
386 387 return commit.size
387 388
388 389 def is_empty(self):
389 390 return self._remote.is_empty()
390 391
391 392 @staticmethod
392 393 def check_url(url, config):
393 394 """
394 395 Function will check given url and try to verify if it's a valid
395 396 link.
396 397 """
397 398 raise NotImplementedError
398 399
399 400 @staticmethod
400 401 def is_valid_repository(path):
401 402 """
402 403 Check if given `path` contains a valid repository of this backend
403 404 """
404 405 raise NotImplementedError
405 406
406 407 # ==========================================================================
407 408 # COMMITS
408 409 # ==========================================================================
409 410
410 411 @CachedProperty('_commit_ids_ver')
411 412 def commit_ids(self):
412 413 raise NotImplementedError
413 414
414 415 def append_commit_id(self, commit_id):
415 416 if commit_id not in self.commit_ids:
416 417 self._rebuild_cache(self.commit_ids + [commit_id])
417 418 self._commit_ids_ver = time.time()
418 419
419 420 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
420 421 """
421 422 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
422 423 are both None, most recent commit is returned.
423 424
424 425 :param pre_load: Optional. List of commit attributes to load.
425 426
426 427 :raises ``EmptyRepositoryError``: if there are no commits
427 428 """
428 429 raise NotImplementedError
429 430
430 431 def __iter__(self):
431 432 for commit_id in self.commit_ids:
432 433 yield self.get_commit(commit_id=commit_id)
433 434
434 435 def get_commits(
435 436 self, start_id=None, end_id=None, start_date=None, end_date=None,
436 437 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
437 438 """
438 439 Returns iterator of `BaseCommit` objects from start to end
439 440 not inclusive. This should behave just like a list, ie. end is not
440 441 inclusive.
441 442
442 443 :param start_id: None or str, must be a valid commit id
443 444 :param end_id: None or str, must be a valid commit id
444 445 :param start_date:
445 446 :param end_date:
446 447 :param branch_name:
447 448 :param show_hidden:
448 449 :param pre_load:
449 450 :param translate_tags:
450 451 """
451 452 raise NotImplementedError
452 453
453 454 def __getitem__(self, key):
454 455 """
455 456 Allows index based access to the commit objects of this repository.
456 457 """
457 458 pre_load = ["author", "branch", "date", "message", "parents"]
458 459 if isinstance(key, slice):
459 460 return self._get_range(key, pre_load)
460 461 return self.get_commit(commit_idx=key, pre_load=pre_load)
461 462
462 463 def _get_range(self, slice_obj, pre_load):
463 464 for commit_id in self.commit_ids.__getitem__(slice_obj):
464 465 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
465 466
466 467 def count(self):
467 468 return len(self.commit_ids)
468 469
469 470 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
470 471 """
471 472 Creates and returns a tag for the given ``commit_id``.
472 473
473 474 :param name: name for new tag
474 475 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
475 476 :param commit_id: commit id for which new tag would be created
476 477 :param message: message of the tag's commit
477 478 :param date: date of tag's commit
478 479
479 480 :raises TagAlreadyExistError: if tag with same name already exists
480 481 """
481 482 raise NotImplementedError
482 483
483 484 def remove_tag(self, name, user, message=None, date=None):
484 485 """
485 486 Removes tag with the given ``name``.
486 487
487 488 :param name: name of the tag to be removed
488 489 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
489 490 :param message: message of the tag's removal commit
490 491 :param date: date of tag's removal commit
491 492
492 493 :raises TagDoesNotExistError: if tag with given name does not exists
493 494 """
494 495 raise NotImplementedError
495 496
496 497 def get_diff(
497 498 self, commit1, commit2, path=None, ignore_whitespace=False,
498 499 context=3, path1=None):
499 500 """
500 501 Returns (git like) *diff*, as plain text. Shows changes introduced by
501 502 `commit2` since `commit1`.
502 503
503 504 :param commit1: Entry point from which diff is shown. Can be
504 505 ``self.EMPTY_COMMIT`` - in this case, patch showing all
505 506 the changes since empty state of the repository until `commit2`
506 507 :param commit2: Until which commit changes should be shown.
507 508 :param path: Can be set to a path of a file to create a diff of that
508 509 file. If `path1` is also set, this value is only associated to
509 510 `commit2`.
510 511 :param ignore_whitespace: If set to ``True``, would not show whitespace
511 512 changes. Defaults to ``False``.
512 513 :param context: How many lines before/after changed lines should be
513 514 shown. Defaults to ``3``.
514 515 :param path1: Can be set to a path to associate with `commit1`. This
515 516 parameter works only for backends which support diff generation for
516 517 different paths. Other backends will raise a `ValueError` if `path1`
517 518 is set and has a different value than `path`.
518 519 :param file_path: filter this diff by given path pattern
519 520 """
520 521 raise NotImplementedError
521 522
522 523 def strip(self, commit_id, branch=None):
523 524 """
524 525 Strip given commit_id from the repository
525 526 """
526 527 raise NotImplementedError
527 528
528 529 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
529 530 """
530 531 Return a latest common ancestor commit if one exists for this repo
531 532 `commit_id1` vs `commit_id2` from `repo2`.
532 533
533 534 :param commit_id1: Commit it from this repository to use as a
534 535 target for the comparison.
535 536 :param commit_id2: Source commit id to use for comparison.
536 537 :param repo2: Source repository to use for comparison.
537 538 """
538 539 raise NotImplementedError
539 540
540 541 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
541 542 """
542 543 Compare this repository's revision `commit_id1` with `commit_id2`.
543 544
544 545 Returns a tuple(commits, ancestor) that would be merged from
545 546 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
546 547 will be returned as ancestor.
547 548
548 549 :param commit_id1: Commit it from this repository to use as a
549 550 target for the comparison.
550 551 :param commit_id2: Source commit id to use for comparison.
551 552 :param repo2: Source repository to use for comparison.
552 553 :param merge: If set to ``True`` will do a merge compare which also
553 554 returns the common ancestor.
554 555 :param pre_load: Optional. List of commit attributes to load.
555 556 """
556 557 raise NotImplementedError
557 558
558 559 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
559 560 user_name='', user_email='', message='', dry_run=False,
560 561 use_rebase=False, close_branch=False):
561 562 """
562 563 Merge the revisions specified in `source_ref` from `source_repo`
563 564 onto the `target_ref` of this repository.
564 565
565 566 `source_ref` and `target_ref` are named tupls with the following
566 567 fields `type`, `name` and `commit_id`.
567 568
568 569 Returns a MergeResponse named tuple with the following fields
569 570 'possible', 'executed', 'source_commit', 'target_commit',
570 571 'merge_commit'.
571 572
572 573 :param repo_id: `repo_id` target repo id.
573 574 :param workspace_id: `workspace_id` unique identifier.
574 575 :param target_ref: `target_ref` points to the commit on top of which
575 576 the `source_ref` should be merged.
576 577 :param source_repo: The repository that contains the commits to be
577 578 merged.
578 579 :param source_ref: `source_ref` points to the topmost commit from
579 580 the `source_repo` which should be merged.
580 581 :param user_name: Merge commit `user_name`.
581 582 :param user_email: Merge commit `user_email`.
582 583 :param message: Merge commit `message`.
583 584 :param dry_run: If `True` the merge will not take place.
584 585 :param use_rebase: If `True` commits from the source will be rebased
585 586 on top of the target instead of being merged.
586 587 :param close_branch: If `True` branch will be close before merging it
587 588 """
588 589 if dry_run:
589 590 message = message or settings.MERGE_DRY_RUN_MESSAGE
590 591 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
591 592 user_name = user_name or settings.MERGE_DRY_RUN_USER
592 593 else:
593 594 if not user_name:
594 595 raise ValueError('user_name cannot be empty')
595 596 if not user_email:
596 597 raise ValueError('user_email cannot be empty')
597 598 if not message:
598 599 raise ValueError('message cannot be empty')
599 600
600 601 try:
601 602 return self._merge_repo(
602 603 repo_id, workspace_id, target_ref, source_repo,
603 604 source_ref, message, user_name, user_email, dry_run=dry_run,
604 605 use_rebase=use_rebase, close_branch=close_branch)
605 606 except RepositoryError as exc:
606 607 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
607 608 return MergeResponse(
608 609 False, False, None, MergeFailureReason.UNKNOWN,
609 610 metadata={'exception': str(exc)})
610 611
611 612 def _merge_repo(self, repo_id, workspace_id, target_ref,
612 613 source_repo, source_ref, merge_message,
613 614 merger_name, merger_email, dry_run=False,
614 615 use_rebase=False, close_branch=False):
615 616 """Internal implementation of merge."""
616 617 raise NotImplementedError
617 618
618 619 def _maybe_prepare_merge_workspace(
619 620 self, repo_id, workspace_id, target_ref, source_ref):
620 621 """
621 622 Create the merge workspace.
622 623
623 624 :param workspace_id: `workspace_id` unique identifier.
624 625 """
625 626 raise NotImplementedError
626 627
627 628 def _get_legacy_shadow_repository_path(self, workspace_id):
628 629 """
629 630 Legacy version that was used before. We still need it for
630 631 backward compat
631 632 """
632 633 return os.path.join(
633 634 os.path.dirname(self.path),
634 635 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
635 636
636 637 def _get_shadow_repository_path(self, repo_id, workspace_id):
637 638 # The name of the shadow repository must start with '.', so it is
638 639 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
639 640 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
640 641 if os.path.exists(legacy_repository_path):
641 642 return legacy_repository_path
642 643 else:
643 644 return os.path.join(
644 645 os.path.dirname(self.path),
645 646 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
646 647
647 648 def cleanup_merge_workspace(self, repo_id, workspace_id):
648 649 """
649 650 Remove merge workspace.
650 651
651 652 This function MUST not fail in case there is no workspace associated to
652 653 the given `workspace_id`.
653 654
654 655 :param workspace_id: `workspace_id` unique identifier.
655 656 """
656 657 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
657 658 shadow_repository_path_del = '{}.{}.delete'.format(
658 659 shadow_repository_path, time.time())
659 660
660 661 # move the shadow repo, so it never conflicts with the one used.
661 662 # we use this method because shutil.rmtree had some edge case problems
662 663 # removing symlinked repositories
663 664 if not os.path.isdir(shadow_repository_path):
664 665 return
665 666
666 667 shutil.move(shadow_repository_path, shadow_repository_path_del)
667 668 try:
668 669 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
669 670 except Exception:
670 671 log.exception('Failed to gracefully remove shadow repo under %s',
671 672 shadow_repository_path_del)
672 673 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
673 674
674 675 # ========== #
675 676 # COMMIT API #
676 677 # ========== #
677 678
678 679 @LazyProperty
679 680 def in_memory_commit(self):
680 681 """
681 682 Returns :class:`InMemoryCommit` object for this repository.
682 683 """
683 684 raise NotImplementedError
684 685
685 686 # ======================== #
686 687 # UTILITIES FOR SUBCLASSES #
687 688 # ======================== #
688 689
689 690 def _validate_diff_commits(self, commit1, commit2):
690 691 """
691 692 Validates that the given commits are related to this repository.
692 693
693 694 Intended as a utility for sub classes to have a consistent validation
694 695 of input parameters in methods like :meth:`get_diff`.
695 696 """
696 697 self._validate_commit(commit1)
697 698 self._validate_commit(commit2)
698 699 if (isinstance(commit1, EmptyCommit) and
699 700 isinstance(commit2, EmptyCommit)):
700 701 raise ValueError("Cannot compare two empty commits")
701 702
702 703 def _validate_commit(self, commit):
703 704 if not isinstance(commit, BaseCommit):
704 705 raise TypeError(
705 706 "%s is not of type BaseCommit" % repr(commit))
706 707 if commit.repository != self and not isinstance(commit, EmptyCommit):
707 708 raise ValueError(
708 709 "Commit %s must be a valid commit from this repository %s, "
709 710 "related to this repository instead %s." %
710 711 (commit, self, commit.repository))
711 712
712 713 def _validate_commit_id(self, commit_id):
713 714 if not isinstance(commit_id, compat.string_types):
714 715 raise TypeError("commit_id must be a string value")
715 716
716 717 def _validate_commit_idx(self, commit_idx):
717 718 if not isinstance(commit_idx, (int, long)):
718 719 raise TypeError("commit_idx must be a numeric value")
719 720
720 721 def _validate_branch_name(self, branch_name):
721 722 if branch_name and branch_name not in self.branches_all:
722 723 msg = ("Branch %s not found in %s" % (branch_name, self))
723 724 raise BranchDoesNotExistError(msg)
724 725
725 726 #
726 727 # Supporting deprecated API parts
727 728 # TODO: johbo: consider to move this into a mixin
728 729 #
729 730
730 731 @property
731 732 def EMPTY_CHANGESET(self):
732 733 warnings.warn(
733 734 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
734 735 return self.EMPTY_COMMIT_ID
735 736
736 737 @property
737 738 def revisions(self):
738 739 warnings.warn("Use commits attribute instead", DeprecationWarning)
739 740 return self.commit_ids
740 741
741 742 @revisions.setter
742 743 def revisions(self, value):
743 744 warnings.warn("Use commits attribute instead", DeprecationWarning)
744 745 self.commit_ids = value
745 746
746 747 def get_changeset(self, revision=None, pre_load=None):
747 748 warnings.warn("Use get_commit instead", DeprecationWarning)
748 749 commit_id = None
749 750 commit_idx = None
750 751 if isinstance(revision, compat.string_types):
751 752 commit_id = revision
752 753 else:
753 754 commit_idx = revision
754 755 return self.get_commit(
755 756 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
756 757
757 758 def get_changesets(
758 759 self, start=None, end=None, start_date=None, end_date=None,
759 760 branch_name=None, pre_load=None):
760 761 warnings.warn("Use get_commits instead", DeprecationWarning)
761 762 start_id = self._revision_to_commit(start)
762 763 end_id = self._revision_to_commit(end)
763 764 return self.get_commits(
764 765 start_id=start_id, end_id=end_id, start_date=start_date,
765 766 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
766 767
767 768 def _revision_to_commit(self, revision):
768 769 """
769 770 Translates a revision to a commit_id
770 771
771 772 Helps to support the old changeset based API which allows to use
772 773 commit ids and commit indices interchangeable.
773 774 """
774 775 if revision is None:
775 776 return revision
776 777
777 778 if isinstance(revision, compat.string_types):
778 779 commit_id = revision
779 780 else:
780 781 commit_id = self.commit_ids[revision]
781 782 return commit_id
782 783
783 784 @property
784 785 def in_memory_changeset(self):
785 786 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
786 787 return self.in_memory_commit
787 788
788 789 def get_path_permissions(self, username):
789 790 """
790 791 Returns a path permission checker or None if not supported
791 792
792 793 :param username: session user name
793 794 :return: an instance of BasePathPermissionChecker or None
794 795 """
795 796 return None
796 797
797 798 def install_hooks(self, force=False):
798 799 return self._remote.install_hooks(force)
799 800
800 801 def get_hooks_info(self):
801 802 return self._remote.get_hooks_info()
802 803
803 804
804 805 class BaseCommit(object):
805 806 """
806 807 Each backend should implement it's commit representation.
807 808
808 809 **Attributes**
809 810
810 811 ``repository``
811 812 repository object within which commit exists
812 813
813 814 ``id``
814 815 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
815 816 just ``tip``.
816 817
817 818 ``raw_id``
818 819 raw commit representation (i.e. full 40 length sha for git
819 820 backend)
820 821
821 822 ``short_id``
822 823 shortened (if apply) version of ``raw_id``; it would be simple
823 824 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
824 825 as ``raw_id`` for subversion
825 826
826 827 ``idx``
827 828 commit index
828 829
829 830 ``files``
830 831 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
831 832
832 833 ``dirs``
833 834 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
834 835
835 836 ``nodes``
836 837 combined list of ``Node`` objects
837 838
838 839 ``author``
839 840 author of the commit, as unicode
840 841
841 842 ``message``
842 843 message of the commit, as unicode
843 844
844 845 ``parents``
845 846 list of parent commits
846 847
847 848 """
848 849
849 850 branch = None
850 851 """
851 852 Depending on the backend this should be set to the branch name of the
852 853 commit. Backends not supporting branches on commits should leave this
853 854 value as ``None``.
854 855 """
855 856
856 857 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
857 858 """
858 859 This template is used to generate a default prefix for repository archives
859 860 if no prefix has been specified.
860 861 """
861 862
862 863 def __str__(self):
863 864 return '<%s at %s:%s>' % (
864 865 self.__class__.__name__, self.idx, self.short_id)
865 866
866 867 def __repr__(self):
867 868 return self.__str__()
868 869
869 870 def __unicode__(self):
870 871 return u'%s:%s' % (self.idx, self.short_id)
871 872
872 873 def __eq__(self, other):
873 874 same_instance = isinstance(other, self.__class__)
874 875 return same_instance and self.raw_id == other.raw_id
875 876
876 877 def __json__(self):
877 878 parents = []
878 879 try:
879 880 for parent in self.parents:
880 881 parents.append({'raw_id': parent.raw_id})
881 882 except NotImplementedError:
882 883 # empty commit doesn't have parents implemented
883 884 pass
884 885
885 886 return {
886 887 'short_id': self.short_id,
887 888 'raw_id': self.raw_id,
888 889 'revision': self.idx,
889 890 'message': self.message,
890 891 'date': self.date,
891 892 'author': self.author,
892 893 'parents': parents,
893 894 'branch': self.branch
894 895 }
895 896
896 897 def __getstate__(self):
897 898 d = self.__dict__.copy()
898 899 d.pop('_remote', None)
899 900 d.pop('repository', None)
900 901 return d
901 902
902 903 def _get_refs(self):
903 904 return {
904 905 'branches': [self.branch] if self.branch else [],
905 906 'bookmarks': getattr(self, 'bookmarks', []),
906 907 'tags': self.tags
907 908 }
908 909
909 910 @LazyProperty
910 911 def last(self):
911 912 """
912 913 ``True`` if this is last commit in repository, ``False``
913 914 otherwise; trying to access this attribute while there is no
914 915 commits would raise `EmptyRepositoryError`
915 916 """
916 917 if self.repository is None:
917 918 raise CommitError("Cannot check if it's most recent commit")
918 919 return self.raw_id == self.repository.commit_ids[-1]
919 920
920 921 @LazyProperty
921 922 def parents(self):
922 923 """
923 924 Returns list of parent commits.
924 925 """
925 926 raise NotImplementedError
926 927
927 928 @LazyProperty
928 929 def first_parent(self):
929 930 """
930 931 Returns list of parent commits.
931 932 """
932 933 return self.parents[0] if self.parents else EmptyCommit()
933 934
934 935 @property
935 936 def merge(self):
936 937 """
937 938 Returns boolean if commit is a merge.
938 939 """
939 940 return len(self.parents) > 1
940 941
941 942 @LazyProperty
942 943 def children(self):
943 944 """
944 945 Returns list of child commits.
945 946 """
946 947 raise NotImplementedError
947 948
948 949 @LazyProperty
949 950 def id(self):
950 951 """
951 952 Returns string identifying this commit.
952 953 """
953 954 raise NotImplementedError
954 955
955 956 @LazyProperty
956 957 def raw_id(self):
957 958 """
958 959 Returns raw string identifying this commit.
959 960 """
960 961 raise NotImplementedError
961 962
962 963 @LazyProperty
963 964 def short_id(self):
964 965 """
965 966 Returns shortened version of ``raw_id`` attribute, as string,
966 967 identifying this commit, useful for presentation to users.
967 968 """
968 969 raise NotImplementedError
969 970
970 971 @LazyProperty
971 972 def idx(self):
972 973 """
973 974 Returns integer identifying this commit.
974 975 """
975 976 raise NotImplementedError
976 977
977 978 @LazyProperty
978 979 def committer(self):
979 980 """
980 981 Returns committer for this commit
981 982 """
982 983 raise NotImplementedError
983 984
984 985 @LazyProperty
985 986 def committer_name(self):
986 987 """
987 988 Returns committer name for this commit
988 989 """
989 990
990 991 return author_name(self.committer)
991 992
992 993 @LazyProperty
993 994 def committer_email(self):
994 995 """
995 996 Returns committer email address for this commit
996 997 """
997 998
998 999 return author_email(self.committer)
999 1000
1000 1001 @LazyProperty
1001 1002 def author(self):
1002 1003 """
1003 1004 Returns author for this commit
1004 1005 """
1005 1006
1006 1007 raise NotImplementedError
1007 1008
1008 1009 @LazyProperty
1009 1010 def author_name(self):
1010 1011 """
1011 1012 Returns author name for this commit
1012 1013 """
1013 1014
1014 1015 return author_name(self.author)
1015 1016
1016 1017 @LazyProperty
1017 1018 def author_email(self):
1018 1019 """
1019 1020 Returns author email address for this commit
1020 1021 """
1021 1022
1022 1023 return author_email(self.author)
1023 1024
1024 1025 def get_file_mode(self, path):
1025 1026 """
1026 1027 Returns stat mode of the file at `path`.
1027 1028 """
1028 1029 raise NotImplementedError
1029 1030
1030 1031 def is_link(self, path):
1031 1032 """
1032 1033 Returns ``True`` if given `path` is a symlink
1033 1034 """
1034 1035 raise NotImplementedError
1035 1036
1036 1037 def get_file_content(self, path):
1037 1038 """
1038 1039 Returns content of the file at the given `path`.
1039 1040 """
1040 1041 raise NotImplementedError
1041 1042
1042 1043 def get_file_size(self, path):
1043 1044 """
1044 1045 Returns size of the file at the given `path`.
1045 1046 """
1046 1047 raise NotImplementedError
1047 1048
1048 1049 def get_path_commit(self, path, pre_load=None):
1049 1050 """
1050 1051 Returns last commit of the file at the given `path`.
1051 1052
1052 1053 :param pre_load: Optional. List of commit attributes to load.
1053 1054 """
1054 1055 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1055 1056 if not commits:
1056 1057 raise RepositoryError(
1057 1058 'Failed to fetch history for path {}. '
1058 1059 'Please check if such path exists in your repository'.format(
1059 1060 path))
1060 1061 return commits[0]
1061 1062
1062 1063 def get_path_history(self, path, limit=None, pre_load=None):
1063 1064 """
1064 1065 Returns history of file as reversed list of :class:`BaseCommit`
1065 1066 objects for which file at given `path` has been modified.
1066 1067
1067 1068 :param limit: Optional. Allows to limit the size of the returned
1068 1069 history. This is intended as a hint to the underlying backend, so
1069 1070 that it can apply optimizations depending on the limit.
1070 1071 :param pre_load: Optional. List of commit attributes to load.
1071 1072 """
1072 1073 raise NotImplementedError
1073 1074
1074 1075 def get_file_annotate(self, path, pre_load=None):
1075 1076 """
1076 1077 Returns a generator of four element tuples with
1077 1078 lineno, sha, commit lazy loader and line
1078 1079
1079 1080 :param pre_load: Optional. List of commit attributes to load.
1080 1081 """
1081 1082 raise NotImplementedError
1082 1083
1083 1084 def get_nodes(self, path):
1084 1085 """
1085 1086 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1086 1087 state of commit at the given ``path``.
1087 1088
1088 1089 :raises ``CommitError``: if node at the given ``path`` is not
1089 1090 instance of ``DirNode``
1090 1091 """
1091 1092 raise NotImplementedError
1092 1093
1093 1094 def get_node(self, path):
1094 1095 """
1095 1096 Returns ``Node`` object from the given ``path``.
1096 1097
1097 1098 :raises ``NodeDoesNotExistError``: if there is no node at the given
1098 1099 ``path``
1099 1100 """
1100 1101 raise NotImplementedError
1101 1102
1102 1103 def get_largefile_node(self, path):
1103 1104 """
1104 1105 Returns the path to largefile from Mercurial/Git-lfs storage.
1105 1106 or None if it's not a largefile node
1106 1107 """
1107 1108 return None
1108 1109
1109 1110 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1110 1111 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1111 1112 """
1112 1113 Creates an archive containing the contents of the repository.
1113 1114
1114 1115 :param archive_dest_path: path to the file which to create the archive.
1115 1116 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1116 1117 :param prefix: name of root directory in archive.
1117 1118 Default is repository name and commit's short_id joined with dash:
1118 1119 ``"{repo_name}-{short_id}"``.
1119 1120 :param write_metadata: write a metadata file into archive.
1120 1121 :param mtime: custom modification time for archive creation, defaults
1121 1122 to time.time() if not given.
1122 1123 :param archive_at_path: pack files at this path (default '/')
1123 1124
1124 1125 :raise VCSError: If prefix has a problem.
1125 1126 """
1126 1127 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1127 1128 if kind not in allowed_kinds:
1128 1129 raise ImproperArchiveTypeError(
1129 1130 'Archive kind (%s) not supported use one of %s' %
1130 1131 (kind, allowed_kinds))
1131 1132
1132 1133 prefix = self._validate_archive_prefix(prefix)
1133 1134
1134 1135 mtime = mtime is not None or time.mktime(self.date.timetuple())
1135 1136
1136 1137 file_info = []
1137 1138 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1138 1139 for _r, _d, files in cur_rev.walk(archive_at_path):
1139 1140 for f in files:
1140 1141 f_path = os.path.join(prefix, f.path)
1141 1142 file_info.append(
1142 1143 (f_path, f.mode, f.is_link(), f.raw_bytes))
1143 1144
1144 1145 if write_metadata:
1145 1146 metadata = [
1146 1147 ('repo_name', self.repository.name),
1147 1148 ('commit_id', self.raw_id),
1148 1149 ('mtime', mtime),
1149 1150 ('branch', self.branch),
1150 1151 ('tags', ','.join(self.tags)),
1151 1152 ]
1152 1153 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1153 1154 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1154 1155
1155 1156 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1156 1157
1157 1158 def _validate_archive_prefix(self, prefix):
1158 1159 if prefix is None:
1159 1160 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1160 1161 repo_name=safe_str(self.repository.name),
1161 1162 short_id=self.short_id)
1162 1163 elif not isinstance(prefix, str):
1163 1164 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1164 1165 elif prefix.startswith('/'):
1165 1166 raise VCSError("Prefix cannot start with leading slash")
1166 1167 elif prefix.strip() == '':
1167 1168 raise VCSError("Prefix cannot be empty")
1168 1169 return prefix
1169 1170
1170 1171 @LazyProperty
1171 1172 def root(self):
1172 1173 """
1173 1174 Returns ``RootNode`` object for this commit.
1174 1175 """
1175 1176 return self.get_node('')
1176 1177
1177 1178 def next(self, branch=None):
1178 1179 """
1179 1180 Returns next commit from current, if branch is gives it will return
1180 1181 next commit belonging to this branch
1181 1182
1182 1183 :param branch: show commits within the given named branch
1183 1184 """
1184 1185 indexes = xrange(self.idx + 1, self.repository.count())
1185 1186 return self._find_next(indexes, branch)
1186 1187
1187 1188 def prev(self, branch=None):
1188 1189 """
1189 1190 Returns previous commit from current, if branch is gives it will
1190 1191 return previous commit belonging to this branch
1191 1192
1192 1193 :param branch: show commit within the given named branch
1193 1194 """
1194 1195 indexes = xrange(self.idx - 1, -1, -1)
1195 1196 return self._find_next(indexes, branch)
1196 1197
1197 1198 def _find_next(self, indexes, branch=None):
1198 1199 if branch and self.branch != branch:
1199 1200 raise VCSError('Branch option used on commit not belonging '
1200 1201 'to that branch')
1201 1202
1202 1203 for next_idx in indexes:
1203 1204 commit = self.repository.get_commit(commit_idx=next_idx)
1204 1205 if branch and branch != commit.branch:
1205 1206 continue
1206 1207 return commit
1207 1208 raise CommitDoesNotExistError
1208 1209
1209 1210 def diff(self, ignore_whitespace=True, context=3):
1210 1211 """
1211 1212 Returns a `Diff` object representing the change made by this commit.
1212 1213 """
1213 1214 parent = self.first_parent
1214 1215 diff = self.repository.get_diff(
1215 1216 parent, self,
1216 1217 ignore_whitespace=ignore_whitespace,
1217 1218 context=context)
1218 1219 return diff
1219 1220
1220 1221 @LazyProperty
1221 1222 def added(self):
1222 1223 """
1223 1224 Returns list of added ``FileNode`` objects.
1224 1225 """
1225 1226 raise NotImplementedError
1226 1227
1227 1228 @LazyProperty
1228 1229 def changed(self):
1229 1230 """
1230 1231 Returns list of modified ``FileNode`` objects.
1231 1232 """
1232 1233 raise NotImplementedError
1233 1234
1234 1235 @LazyProperty
1235 1236 def removed(self):
1236 1237 """
1237 1238 Returns list of removed ``FileNode`` objects.
1238 1239 """
1239 1240 raise NotImplementedError
1240 1241
1241 1242 @LazyProperty
1242 1243 def size(self):
1243 1244 """
1244 1245 Returns total number of bytes from contents of all filenodes.
1245 1246 """
1246 1247 return sum((node.size for node in self.get_filenodes_generator()))
1247 1248
1248 1249 def walk(self, topurl=''):
1249 1250 """
1250 1251 Similar to os.walk method. Insted of filesystem it walks through
1251 1252 commit starting at given ``topurl``. Returns generator of tuples
1252 1253 (topnode, dirnodes, filenodes).
1253 1254 """
1254 1255 topnode = self.get_node(topurl)
1255 1256 if not topnode.is_dir():
1256 1257 return
1257 1258 yield (topnode, topnode.dirs, topnode.files)
1258 1259 for dirnode in topnode.dirs:
1259 1260 for tup in self.walk(dirnode.path):
1260 1261 yield tup
1261 1262
1262 1263 def get_filenodes_generator(self):
1263 1264 """
1264 1265 Returns generator that yields *all* file nodes.
1265 1266 """
1266 1267 for topnode, dirs, files in self.walk():
1267 1268 for node in files:
1268 1269 yield node
1269 1270
1270 1271 #
1271 1272 # Utilities for sub classes to support consistent behavior
1272 1273 #
1273 1274
1274 1275 def no_node_at_path(self, path):
1275 1276 return NodeDoesNotExistError(
1276 1277 u"There is no file nor directory at the given path: "
1277 1278 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1278 1279
1279 1280 def _fix_path(self, path):
1280 1281 """
1281 1282 Paths are stored without trailing slash so we need to get rid off it if
1282 1283 needed.
1283 1284 """
1284 1285 return path.rstrip('/')
1285 1286
1286 1287 #
1287 1288 # Deprecated API based on changesets
1288 1289 #
1289 1290
1290 1291 @property
1291 1292 def revision(self):
1292 1293 warnings.warn("Use idx instead", DeprecationWarning)
1293 1294 return self.idx
1294 1295
1295 1296 @revision.setter
1296 1297 def revision(self, value):
1297 1298 warnings.warn("Use idx instead", DeprecationWarning)
1298 1299 self.idx = value
1299 1300
1300 1301 def get_file_changeset(self, path):
1301 1302 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1302 1303 return self.get_path_commit(path)
1303 1304
1304 1305
1305 1306 class BaseChangesetClass(type):
1306 1307
1307 1308 def __instancecheck__(self, instance):
1308 1309 return isinstance(instance, BaseCommit)
1309 1310
1310 1311
1311 1312 class BaseChangeset(BaseCommit):
1312 1313
1313 1314 __metaclass__ = BaseChangesetClass
1314 1315
1315 1316 def __new__(cls, *args, **kwargs):
1316 1317 warnings.warn(
1317 1318 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1318 1319 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1319 1320
1320 1321
1321 1322 class BaseInMemoryCommit(object):
1322 1323 """
1323 1324 Represents differences between repository's state (most recent head) and
1324 1325 changes made *in place*.
1325 1326
1326 1327 **Attributes**
1327 1328
1328 1329 ``repository``
1329 1330 repository object for this in-memory-commit
1330 1331
1331 1332 ``added``
1332 1333 list of ``FileNode`` objects marked as *added*
1333 1334
1334 1335 ``changed``
1335 1336 list of ``FileNode`` objects marked as *changed*
1336 1337
1337 1338 ``removed``
1338 1339 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1339 1340 *removed*
1340 1341
1341 1342 ``parents``
1342 1343 list of :class:`BaseCommit` instances representing parents of
1343 1344 in-memory commit. Should always be 2-element sequence.
1344 1345
1345 1346 """
1346 1347
1347 1348 def __init__(self, repository):
1348 1349 self.repository = repository
1349 1350 self.added = []
1350 1351 self.changed = []
1351 1352 self.removed = []
1352 1353 self.parents = []
1353 1354
1354 1355 def add(self, *filenodes):
1355 1356 """
1356 1357 Marks given ``FileNode`` objects as *to be committed*.
1357 1358
1358 1359 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1359 1360 latest commit
1360 1361 :raises ``NodeAlreadyAddedError``: if node with same path is already
1361 1362 marked as *added*
1362 1363 """
1363 1364 # Check if not already marked as *added* first
1364 1365 for node in filenodes:
1365 1366 if node.path in (n.path for n in self.added):
1366 1367 raise NodeAlreadyAddedError(
1367 1368 "Such FileNode %s is already marked for addition"
1368 1369 % node.path)
1369 1370 for node in filenodes:
1370 1371 self.added.append(node)
1371 1372
1372 1373 def change(self, *filenodes):
1373 1374 """
1374 1375 Marks given ``FileNode`` objects to be *changed* in next commit.
1375 1376
1376 1377 :raises ``EmptyRepositoryError``: if there are no commits yet
1377 1378 :raises ``NodeAlreadyExistsError``: if node with same path is already
1378 1379 marked to be *changed*
1379 1380 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1380 1381 marked to be *removed*
1381 1382 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1382 1383 commit
1383 1384 :raises ``NodeNotChangedError``: if node hasn't really be changed
1384 1385 """
1385 1386 for node in filenodes:
1386 1387 if node.path in (n.path for n in self.removed):
1387 1388 raise NodeAlreadyRemovedError(
1388 1389 "Node at %s is already marked as removed" % node.path)
1389 1390 try:
1390 1391 self.repository.get_commit()
1391 1392 except EmptyRepositoryError:
1392 1393 raise EmptyRepositoryError(
1393 1394 "Nothing to change - try to *add* new nodes rather than "
1394 1395 "changing them")
1395 1396 for node in filenodes:
1396 1397 if node.path in (n.path for n in self.changed):
1397 1398 raise NodeAlreadyChangedError(
1398 1399 "Node at '%s' is already marked as changed" % node.path)
1399 1400 self.changed.append(node)
1400 1401
1401 1402 def remove(self, *filenodes):
1402 1403 """
1403 1404 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1404 1405 *removed* in next commit.
1405 1406
1406 1407 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1407 1408 be *removed*
1408 1409 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1409 1410 be *changed*
1410 1411 """
1411 1412 for node in filenodes:
1412 1413 if node.path in (n.path for n in self.removed):
1413 1414 raise NodeAlreadyRemovedError(
1414 1415 "Node is already marked to for removal at %s" % node.path)
1415 1416 if node.path in (n.path for n in self.changed):
1416 1417 raise NodeAlreadyChangedError(
1417 1418 "Node is already marked to be changed at %s" % node.path)
1418 1419 # We only mark node as *removed* - real removal is done by
1419 1420 # commit method
1420 1421 self.removed.append(node)
1421 1422
1422 1423 def reset(self):
1423 1424 """
1424 1425 Resets this instance to initial state (cleans ``added``, ``changed``
1425 1426 and ``removed`` lists).
1426 1427 """
1427 1428 self.added = []
1428 1429 self.changed = []
1429 1430 self.removed = []
1430 1431 self.parents = []
1431 1432
1432 1433 def get_ipaths(self):
1433 1434 """
1434 1435 Returns generator of paths from nodes marked as added, changed or
1435 1436 removed.
1436 1437 """
1437 1438 for node in itertools.chain(self.added, self.changed, self.removed):
1438 1439 yield node.path
1439 1440
1440 1441 def get_paths(self):
1441 1442 """
1442 1443 Returns list of paths from nodes marked as added, changed or removed.
1443 1444 """
1444 1445 return list(self.get_ipaths())
1445 1446
1446 1447 def check_integrity(self, parents=None):
1447 1448 """
1448 1449 Checks in-memory commit's integrity. Also, sets parents if not
1449 1450 already set.
1450 1451
1451 1452 :raises CommitError: if any error occurs (i.e.
1452 1453 ``NodeDoesNotExistError``).
1453 1454 """
1454 1455 if not self.parents:
1455 1456 parents = parents or []
1456 1457 if len(parents) == 0:
1457 1458 try:
1458 1459 parents = [self.repository.get_commit(), None]
1459 1460 except EmptyRepositoryError:
1460 1461 parents = [None, None]
1461 1462 elif len(parents) == 1:
1462 1463 parents += [None]
1463 1464 self.parents = parents
1464 1465
1465 1466 # Local parents, only if not None
1466 1467 parents = [p for p in self.parents if p]
1467 1468
1468 1469 # Check nodes marked as added
1469 1470 for p in parents:
1470 1471 for node in self.added:
1471 1472 try:
1472 1473 p.get_node(node.path)
1473 1474 except NodeDoesNotExistError:
1474 1475 pass
1475 1476 else:
1476 1477 raise NodeAlreadyExistsError(
1477 1478 "Node `%s` already exists at %s" % (node.path, p))
1478 1479
1479 1480 # Check nodes marked as changed
1480 1481 missing = set(self.changed)
1481 1482 not_changed = set(self.changed)
1482 1483 if self.changed and not parents:
1483 1484 raise NodeDoesNotExistError(str(self.changed[0].path))
1484 1485 for p in parents:
1485 1486 for node in self.changed:
1486 1487 try:
1487 1488 old = p.get_node(node.path)
1488 1489 missing.remove(node)
1489 1490 # if content actually changed, remove node from not_changed
1490 1491 if old.content != node.content:
1491 1492 not_changed.remove(node)
1492 1493 except NodeDoesNotExistError:
1493 1494 pass
1494 1495 if self.changed and missing:
1495 1496 raise NodeDoesNotExistError(
1496 1497 "Node `%s` marked as modified but missing in parents: %s"
1497 1498 % (node.path, parents))
1498 1499
1499 1500 if self.changed and not_changed:
1500 1501 raise NodeNotChangedError(
1501 1502 "Node `%s` wasn't actually changed (parents: %s)"
1502 1503 % (not_changed.pop().path, parents))
1503 1504
1504 1505 # Check nodes marked as removed
1505 1506 if self.removed and not parents:
1506 1507 raise NodeDoesNotExistError(
1507 1508 "Cannot remove node at %s as there "
1508 1509 "were no parents specified" % self.removed[0].path)
1509 1510 really_removed = set()
1510 1511 for p in parents:
1511 1512 for node in self.removed:
1512 1513 try:
1513 1514 p.get_node(node.path)
1514 1515 really_removed.add(node)
1515 1516 except CommitError:
1516 1517 pass
1517 1518 not_removed = set(self.removed) - really_removed
1518 1519 if not_removed:
1519 1520 # TODO: johbo: This code branch does not seem to be covered
1520 1521 raise NodeDoesNotExistError(
1521 1522 "Cannot remove node at %s from "
1522 1523 "following parents: %s" % (not_removed, parents))
1523 1524
1524 1525 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1525 1526 """
1526 1527 Performs in-memory commit (doesn't check workdir in any way) and
1527 1528 returns newly created :class:`BaseCommit`. Updates repository's
1528 1529 attribute `commits`.
1529 1530
1530 1531 .. note::
1531 1532
1532 1533 While overriding this method each backend's should call
1533 1534 ``self.check_integrity(parents)`` in the first place.
1534 1535
1535 1536 :param message: message of the commit
1536 1537 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1537 1538 :param parents: single parent or sequence of parents from which commit
1538 1539 would be derived
1539 1540 :param date: ``datetime.datetime`` instance. Defaults to
1540 1541 ``datetime.datetime.now()``.
1541 1542 :param branch: branch name, as string. If none given, default backend's
1542 1543 branch would be used.
1543 1544
1544 1545 :raises ``CommitError``: if any error occurs while committing
1545 1546 """
1546 1547 raise NotImplementedError
1547 1548
1548 1549
1549 1550 class BaseInMemoryChangesetClass(type):
1550 1551
1551 1552 def __instancecheck__(self, instance):
1552 1553 return isinstance(instance, BaseInMemoryCommit)
1553 1554
1554 1555
1555 1556 class BaseInMemoryChangeset(BaseInMemoryCommit):
1556 1557
1557 1558 __metaclass__ = BaseInMemoryChangesetClass
1558 1559
1559 1560 def __new__(cls, *args, **kwargs):
1560 1561 warnings.warn(
1561 1562 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1562 1563 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1563 1564
1564 1565
1565 1566 class EmptyCommit(BaseCommit):
1566 1567 """
1567 1568 An dummy empty commit. It's possible to pass hash when creating
1568 1569 an EmptyCommit
1569 1570 """
1570 1571
1571 1572 def __init__(
1572 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1573 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1573 1574 message='', author='', date=None):
1574 1575 self._empty_commit_id = commit_id
1575 1576 # TODO: johbo: Solve idx parameter, default value does not make
1576 1577 # too much sense
1577 1578 self.idx = idx
1578 1579 self.message = message
1579 1580 self.author = author
1580 1581 self.date = date or datetime.datetime.fromtimestamp(0)
1581 1582 self.repository = repo
1582 1583 self.alias = alias
1583 1584
1584 1585 @LazyProperty
1585 1586 def raw_id(self):
1586 1587 """
1587 1588 Returns raw string identifying this commit, useful for web
1588 1589 representation.
1589 1590 """
1590 1591
1591 1592 return self._empty_commit_id
1592 1593
1593 1594 @LazyProperty
1594 1595 def branch(self):
1595 1596 if self.alias:
1596 1597 from rhodecode.lib.vcs.backends import get_backend
1597 1598 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1598 1599
1599 1600 @LazyProperty
1600 1601 def short_id(self):
1601 1602 return self.raw_id[:12]
1602 1603
1603 1604 @LazyProperty
1604 1605 def id(self):
1605 1606 return self.raw_id
1606 1607
1607 1608 def get_path_commit(self, path):
1608 1609 return self
1609 1610
1610 1611 def get_file_content(self, path):
1611 1612 return u''
1612 1613
1613 1614 def get_file_size(self, path):
1614 1615 return 0
1615 1616
1616 1617
1617 1618 class EmptyChangesetClass(type):
1618 1619
1619 1620 def __instancecheck__(self, instance):
1620 1621 return isinstance(instance, EmptyCommit)
1621 1622
1622 1623
1623 1624 class EmptyChangeset(EmptyCommit):
1624 1625
1625 1626 __metaclass__ = EmptyChangesetClass
1626 1627
1627 1628 def __new__(cls, *args, **kwargs):
1628 1629 warnings.warn(
1629 1630 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1630 1631 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1631 1632
1632 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1633 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1633 1634 alias=None, revision=-1, message='', author='', date=None):
1634 1635 if requested_revision is not None:
1635 1636 warnings.warn(
1636 1637 "Parameter requested_revision not supported anymore",
1637 1638 DeprecationWarning)
1638 1639 super(EmptyChangeset, self).__init__(
1639 1640 commit_id=cs, repo=repo, alias=alias, idx=revision,
1640 1641 message=message, author=author, date=date)
1641 1642
1642 1643 @property
1643 1644 def revision(self):
1644 1645 warnings.warn("Use idx instead", DeprecationWarning)
1645 1646 return self.idx
1646 1647
1647 1648 @revision.setter
1648 1649 def revision(self, value):
1649 1650 warnings.warn("Use idx instead", DeprecationWarning)
1650 1651 self.idx = value
1651 1652
1652 1653
1653 1654 class EmptyRepository(BaseRepository):
1654 1655 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1655 1656 pass
1656 1657
1657 1658 def get_diff(self, *args, **kwargs):
1658 1659 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1659 1660 return GitDiff('')
1660 1661
1661 1662
1662 1663 class CollectionGenerator(object):
1663 1664
1664 1665 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1665 1666 self.repo = repo
1666 1667 self.commit_ids = commit_ids
1667 1668 # TODO: (oliver) this isn't currently hooked up
1668 1669 self.collection_size = None
1669 1670 self.pre_load = pre_load
1670 1671 self.translate_tag = translate_tag
1671 1672
1672 1673 def __len__(self):
1673 1674 if self.collection_size is not None:
1674 1675 return self.collection_size
1675 1676 return self.commit_ids.__len__()
1676 1677
1677 1678 def __iter__(self):
1678 1679 for commit_id in self.commit_ids:
1679 1680 # TODO: johbo: Mercurial passes in commit indices or commit ids
1680 1681 yield self._commit_factory(commit_id)
1681 1682
1682 1683 def _commit_factory(self, commit_id):
1683 1684 """
1684 1685 Allows backends to override the way commits are generated.
1685 1686 """
1686 1687 return self.repo.get_commit(
1687 1688 commit_id=commit_id, pre_load=self.pre_load,
1688 1689 translate_tag=self.translate_tag)
1689 1690
1690 1691 def __getslice__(self, i, j):
1691 1692 """
1692 1693 Returns an iterator of sliced repository
1693 1694 """
1694 1695 commit_ids = self.commit_ids[i:j]
1695 1696 return self.__class__(
1696 1697 self.repo, commit_ids, pre_load=self.pre_load,
1697 1698 translate_tag=self.translate_tag)
1698 1699
1699 1700 def __repr__(self):
1700 1701 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1701 1702
1702 1703
1703 1704 class Config(object):
1704 1705 """
1705 1706 Represents the configuration for a repository.
1706 1707
1707 1708 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1708 1709 standard library. It implements only the needed subset.
1709 1710 """
1710 1711
1711 1712 def __init__(self):
1712 1713 self._values = {}
1713 1714
1714 1715 def copy(self):
1715 1716 clone = Config()
1716 1717 for section, values in self._values.items():
1717 1718 clone._values[section] = values.copy()
1718 1719 return clone
1719 1720
1720 1721 def __repr__(self):
1721 1722 return '<Config(%s sections) at %s>' % (
1722 1723 len(self._values), hex(id(self)))
1723 1724
1724 1725 def items(self, section):
1725 1726 return self._values.get(section, {}).iteritems()
1726 1727
1727 1728 def get(self, section, option):
1728 1729 return self._values.get(section, {}).get(option)
1729 1730
1730 1731 def set(self, section, option, value):
1731 1732 section_values = self._values.setdefault(section, {})
1732 1733 section_values[option] = value
1733 1734
1734 1735 def clear_section(self, section):
1735 1736 self._values[section] = {}
1736 1737
1737 1738 def serialize(self):
1738 1739 """
1739 1740 Creates a list of three tuples (section, key, value) representing
1740 1741 this config object.
1741 1742 """
1742 1743 items = []
1743 1744 for section in self._values:
1744 1745 for option, value in self._values[section].items():
1745 1746 items.append(
1746 1747 (safe_str(section), safe_str(option), safe_str(value)))
1747 1748 return items
1748 1749
1749 1750
1750 1751 class Diff(object):
1751 1752 """
1752 1753 Represents a diff result from a repository backend.
1753 1754
1754 1755 Subclasses have to provide a backend specific value for
1755 1756 :attr:`_header_re` and :attr:`_meta_re`.
1756 1757 """
1757 1758 _meta_re = None
1758 1759 _header_re = None
1759 1760
1760 1761 def __init__(self, raw_diff):
1761 1762 self.raw = raw_diff
1762 1763
1763 1764 def chunks(self):
1764 1765 """
1765 1766 split the diff in chunks of separate --git a/file b/file chunks
1766 1767 to make diffs consistent we must prepend with \n, and make sure
1767 1768 we can detect last chunk as this was also has special rule
1768 1769 """
1769 1770
1770 1771 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1771 1772 header = diff_parts[0]
1772 1773
1773 1774 if self._meta_re:
1774 1775 match = self._meta_re.match(header)
1775 1776
1776 1777 chunks = diff_parts[1:]
1777 1778 total_chunks = len(chunks)
1778 1779
1779 1780 return (
1780 1781 DiffChunk(chunk, self, cur_chunk == total_chunks)
1781 1782 for cur_chunk, chunk in enumerate(chunks, start=1))
1782 1783
1783 1784
1784 1785 class DiffChunk(object):
1785 1786
1786 1787 def __init__(self, chunk, diff, last_chunk):
1787 1788 self._diff = diff
1788 1789
1789 1790 # since we split by \ndiff --git that part is lost from original diff
1790 1791 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1791 1792 if not last_chunk:
1792 1793 chunk += '\n'
1793 1794
1794 1795 match = self._diff._header_re.match(chunk)
1795 1796 self.header = match.groupdict()
1796 1797 self.diff = chunk[match.end():]
1797 1798 self.raw = chunk
1798 1799
1799 1800
1800 1801 class BasePathPermissionChecker(object):
1801 1802
1802 1803 @staticmethod
1803 1804 def create_from_patterns(includes, excludes):
1804 1805 if includes and '*' in includes and not excludes:
1805 1806 return AllPathPermissionChecker()
1806 1807 elif excludes and '*' in excludes:
1807 1808 return NonePathPermissionChecker()
1808 1809 else:
1809 1810 return PatternPathPermissionChecker(includes, excludes)
1810 1811
1811 1812 @property
1812 1813 def has_full_access(self):
1813 1814 raise NotImplemented()
1814 1815
1815 1816 def has_access(self, path):
1816 1817 raise NotImplemented()
1817 1818
1818 1819
1819 1820 class AllPathPermissionChecker(BasePathPermissionChecker):
1820 1821
1821 1822 @property
1822 1823 def has_full_access(self):
1823 1824 return True
1824 1825
1825 1826 def has_access(self, path):
1826 1827 return True
1827 1828
1828 1829
1829 1830 class NonePathPermissionChecker(BasePathPermissionChecker):
1830 1831
1831 1832 @property
1832 1833 def has_full_access(self):
1833 1834 return False
1834 1835
1835 1836 def has_access(self, path):
1836 1837 return False
1837 1838
1838 1839
1839 1840 class PatternPathPermissionChecker(BasePathPermissionChecker):
1840 1841
1841 1842 def __init__(self, includes, excludes):
1842 1843 self.includes = includes
1843 1844 self.excludes = excludes
1844 1845 self.includes_re = [] if not includes else [
1845 1846 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1846 1847 self.excludes_re = [] if not excludes else [
1847 1848 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1848 1849
1849 1850 @property
1850 1851 def has_full_access(self):
1851 1852 return '*' in self.includes and not self.excludes
1852 1853
1853 1854 def has_access(self, path):
1854 1855 for regex in self.excludes_re:
1855 1856 if regex.match(path):
1856 1857 return False
1857 1858 for regex in self.includes_re:
1858 1859 if regex.match(path):
1859 1860 return True
1860 1861 return False
@@ -1,311 +1,311 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4 4
5 5 <%def name="title()">
6 6 %if c.compare_home:
7 7 ${_('%s Compare') % c.repo_name}
8 8 %else:
9 9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 10 %endif
11 11 %if c.rhodecode_name:
12 12 &middot; ${h.branding(c.rhodecode_name)}
13 13 %endif
14 14 </%def>
15 15
16 16 <%def name="breadcrumbs_links()"></%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='compare')}
24 24 </%def>
25 25
26 26 <%def name="main()">
27 27 <script type="text/javascript">
28 28 // set fake commitId on this commit-range page
29 29 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
30 30 </script>
31 31
32 32 <div class="box">
33 33 <div class="summary changeset">
34 34 <div class="summary-detail">
35 35 <div class="summary-detail-header">
36 36 <span class="breadcrumbs files_location">
37 37 <h4>
38 38 ${_('Compare Commits')}
39 39 % if c.file_path:
40 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
40 ${_('for file')} <a href="#${('a_' + h.FID('',c.file_path))}">${c.file_path}</a>
41 41 % endif
42 42
43 43 % if c.commit_ranges:
44 44 <code>
45 r${c.source_commit.idx}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.idx}:${h.short_id(c.target_commit.raw_id)}
45 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
46 46 </code>
47 47 % endif
48 48 </h4>
49 49 </span>
50 50
51 51 <div class="clear-fix"></div>
52 52 </div>
53 53
54 54 <div class="fieldset">
55 55 <div class="left-label-summary">
56 56 <p class="spacing">${_('Target')}:</p>
57 57 <div class="right-label-summary">
58 58 <div class="code-header" >
59 59 <div class="compare_header">
60 60 ## The hidden elements are replaced with a select2 widget
61 61 ${h.hidden('compare_source')}
62 62 </div>
63 63 </div>
64 64 </div>
65 65 </div>
66 66 </div>
67 67
68 68 <div class="fieldset">
69 69 <div class="left-label-summary">
70 70 <p class="spacing">${_('Source')}:</p>
71 71 <div class="right-label-summary">
72 72 <div class="code-header" >
73 73 <div class="compare_header">
74 74 ## The hidden elements are replaced with a select2 widget
75 75 ${h.hidden('compare_target')}
76 76 </div>
77 77 </div>
78 78 </div>
79 79 </div>
80 80 </div>
81 81
82 82 <div class="fieldset">
83 83 <div class="left-label-summary">
84 84 <p class="spacing">${_('Actions')}:</p>
85 85 <div class="right-label-summary">
86 86 <div class="code-header" >
87 87 <div class="compare_header">
88 88 <div class="compare-buttons">
89 89 % if c.compare_home:
90 90 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
91 91 %if c.rhodecode_db_repo.fork:
92 92
93 93 <a class="btn btn-default" title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
94 94 href="${h.route_path('repo_compare',
95 95 repo_name=c.rhodecode_db_repo.fork.repo_name,
96 96 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
97 97 source_ref=c.rhodecode_db_repo.landing_rev[1],
98 98 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
99 99 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
100 100 _query=dict(merge=1))}"
101 101 >
102 102 ${_('Compare with origin')}
103 103 </a>
104 104
105 105 %endif
106 106
107 107 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
108 108 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
109 109 <div id="changeset_compare_view_content">
110 110 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
111 111 </div>
112 112
113 113 % elif c.preview_mode:
114 114 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
115 115 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
116 116 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
117 117
118 118 % else:
119 119 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
120 120 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
121 121
122 122 ## allow comment only if there are commits to comment on
123 123 % if c.diffset and c.diffset.files and c.commit_ranges:
124 124 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
125 125 % else:
126 126 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
127 127 % endif
128 128 % endif
129 129 </div>
130 130 </div>
131 131 </div>
132 132 </div>
133 133 </div>
134 134 </div>
135 135
136 136 ## commit status form
137 137 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
138 138 <div class="left-label-summary">
139 139 <p class="spacing">${_('Commit status')}:</p>
140 140 <div class="right-label-summary">
141 141 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
142 142 ## main comment form and it status
143 143 <%
144 144 def revs(_revs):
145 145 form_inputs = []
146 146 for cs in _revs:
147 147 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
148 148 form_inputs.append(tmpl)
149 149 return form_inputs
150 150 %>
151 151 <div>
152 152 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="clear-fix"></div>
158 158 </div> <!-- end summary-detail -->
159 159 </div> <!-- end summary -->
160 160
161 161 ## use JS script to load it quickly before potentially large diffs render long time
162 162 ## this prevents from situation when large diffs block rendering of select2 fields
163 163 <script type="text/javascript">
164 164
165 165 var cache = {};
166 166
167 167 var formatSelection = function(repoName){
168 168 return function(data, container, escapeMarkup) {
169 169 var selection = data ? this.text(data) : "";
170 170 return escapeMarkup('{0}@{1}'.format(repoName, selection));
171 171 }
172 172 };
173 173
174 174 var feedCompareData = function(query, cachedValue){
175 175 var data = {results: []};
176 176 //filter results
177 177 $.each(cachedValue.results, function() {
178 178 var section = this.text;
179 179 var children = [];
180 180 $.each(this.children, function() {
181 181 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
182 182 children.push({
183 183 'id': this.id,
184 184 'text': this.text,
185 185 'type': this.type
186 186 })
187 187 }
188 188 });
189 189 data.results.push({
190 190 'text': section,
191 191 'children': children
192 192 })
193 193 });
194 194 //push the typed in changeset
195 195 data.results.push({
196 196 'text': _gettext('specify commit'),
197 197 'children': [{
198 198 'id': query.term,
199 199 'text': query.term,
200 200 'type': 'rev'
201 201 }]
202 202 });
203 203 query.callback(data);
204 204 };
205 205
206 206 var loadCompareData = function(repoName, query, cache){
207 207 $.ajax({
208 208 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
209 209 data: {},
210 210 dataType: 'json',
211 211 type: 'GET',
212 212 success: function(data) {
213 213 cache[repoName] = data;
214 214 query.callback({results: data.results});
215 215 }
216 216 })
217 217 };
218 218
219 219 var enable_fields = ${"false" if c.preview_mode else "true"};
220 220 $("#compare_source").select2({
221 221 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
222 222 containerCssClass: "drop-menu",
223 223 dropdownCssClass: "drop-menu-dropdown",
224 224 formatSelection: formatSelection("${c.source_repo.repo_name}"),
225 225 dropdownAutoWidth: true,
226 226 query: function(query) {
227 227 var repoName = '${c.source_repo.repo_name}';
228 228 var cachedValue = cache[repoName];
229 229
230 230 if (cachedValue){
231 231 feedCompareData(query, cachedValue);
232 232 }
233 233 else {
234 234 loadCompareData(repoName, query, cache);
235 235 }
236 236 }
237 237 }).select2("enable", enable_fields);
238 238
239 239 $("#compare_target").select2({
240 240 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
241 241 dropdownAutoWidth: true,
242 242 containerCssClass: "drop-menu",
243 243 dropdownCssClass: "drop-menu-dropdown",
244 244 formatSelection: formatSelection("${c.target_repo.repo_name}"),
245 245 query: function(query) {
246 246 var repoName = '${c.target_repo.repo_name}';
247 247 var cachedValue = cache[repoName];
248 248
249 249 if (cachedValue){
250 250 feedCompareData(query, cachedValue);
251 251 }
252 252 else {
253 253 loadCompareData(repoName, query, cache);
254 254 }
255 255 }
256 256 }).select2("enable", enable_fields);
257 257 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
258 258 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
259 259
260 260 $('#compare_revs').on('click', function(e) {
261 261 var source = $('#compare_source').select2('data') || initial_compare_source;
262 262 var target = $('#compare_target').select2('data') || initial_compare_target;
263 263 if (source && target) {
264 264 var url_data = {
265 265 repo_name: "${c.repo_name}",
266 266 source_ref: source.id,
267 267 source_ref_type: source.type,
268 268 target_ref: target.id,
269 269 target_ref_type: target.type
270 270 };
271 271 window.location = pyroutes.url('repo_compare', url_data);
272 272 }
273 273 });
274 274 $('#compare_changeset_status_toggle').on('click', function(e) {
275 275 $('#compare_changeset_status').toggle();
276 276 });
277 277
278 278 </script>
279 279
280 280 ## table diff data
281 281 <div class="table">
282 282 % if not c.compare_home:
283 283 <div id="changeset_compare_view_content">
284 284 <div class="pull-left">
285 285 <div class="btn-group">
286 286 <a
287 287 class="btn"
288 288 href="#"
289 289 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
290 290 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
291 291 </a>
292 292 <a
293 293 class="btn"
294 294 href="#"
295 295 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
296 296 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
297 297 </a>
298 298 </div>
299 299 </div>
300 300 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
301 301 ## commit compare generated below
302 302 <%include file="compare_commits.mako"/>
303 303 ${cbdiffs.render_diffset_menu(c.diffset)}
304 304 ${cbdiffs.render_diffset(c.diffset)}
305 305 </div>
306 306 % endif
307 307
308 308 </div>
309 309 </div>
310 310
311 311 </%def>
@@ -1,464 +1,467 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import threading
22 22 import time
23 23 import logging
24 24 import os.path
25 25 import subprocess32
26 26 import tempfile
27 27 import urllib2
28 28 from lxml.html import fromstring, tostring
29 29 from lxml.cssselect import CSSSelector
30 30 from urlparse import urlparse, parse_qsl
31 31 from urllib import unquote_plus
32 32 import webob
33 33
34 34 from webtest.app import TestResponse, TestApp, string_types
35 35 from webtest.compat import print_stderr
36 36
37 37 import pytest
38 38 import rc_testdata
39 39
40 40 from rhodecode.model.db import User, Repository
41 41 from rhodecode.model.meta import Session
42 42 from rhodecode.model.scm import ScmModel
43 43 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.tests import login_user_session
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class CustomTestResponse(TestResponse):
51
51 52 def _save_output(self, out):
52 f = tempfile.NamedTemporaryFile(
53 delete=False, prefix='rc-test-', suffix='.html')
53 f = tempfile.NamedTemporaryFile(delete=False, prefix='rc-test-', suffix='.html')
54 54 f.write(out)
55 55 return f.name
56 56
57 57 def mustcontain(self, *strings, **kw):
58 58 """
59 59 Assert that the response contains all of the strings passed
60 60 in as arguments.
61 61
62 62 Equivalent to::
63 63
64 64 assert string in res
65 65 """
66 print_body = kw.pop('print_body', False)
66 67 if 'no' in kw:
67 68 no = kw['no']
68 69 del kw['no']
69 70 if isinstance(no, string_types):
70 71 no = [no]
71 72 else:
72 73 no = []
73 74 if kw:
74 75 raise TypeError(
75 76 "The only keyword argument allowed is 'no' got %s" % kw)
76 77
77 78 f = self._save_output(str(self))
78 79
79 80 for s in strings:
80 81 if not s in self:
81 82 print_stderr("Actual response (no %r):" % s)
82 print_stderr(str(self))
83 print_stderr("body output saved as `%s`" % f)
84 if print_body:
85 print_stderr(str(self))
83 86 raise IndexError(
84 "Body does not contain string %r, output saved as %s" % (
85 s, f))
87 "Body does not contain string %r, body output saved as %s" % (s, f))
86 88
87 89 for no_s in no:
88 90 if no_s in self:
89 91 print_stderr("Actual response (has %r)" % no_s)
90 print_stderr(str(self))
92 print_stderr("body output saved as `%s`" % f)
93 if print_body:
94 print_stderr(str(self))
91 95 raise IndexError(
92 "Body contains bad string %r, output saved as %s" % (
93 no_s, f))
96 "Body contains bad string %r, body output saved as %s" % (no_s, f))
94 97
95 98 def assert_response(self):
96 99 return AssertResponse(self)
97 100
98 101 def get_session_from_response(self):
99 102 """
100 103 This returns the session from a response object.
101 104 """
102 105 from rhodecode.lib.rc_beaker import session_factory_from_settings
103 106 session = session_factory_from_settings(self.test_app._pyramid_settings)
104 107 return session(self.request)
105 108
106 109
107 110 class TestRequest(webob.BaseRequest):
108 111
109 112 # for py.test
110 113 disabled = True
111 114 ResponseClass = CustomTestResponse
112 115
113 116 def add_response_callback(self, callback):
114 117 pass
115 118
116 119
117 120 class CustomTestApp(TestApp):
118 121 """
119 122 Custom app to make mustcontain more Useful, and extract special methods
120 123 """
121 124 RequestClass = TestRequest
122 125 rc_login_data = {}
123 126 rc_current_session = None
124 127
125 128 def login(self, username=None, password=None):
126 129 from rhodecode.lib import auth
127 130
128 131 if username and password:
129 132 session = login_user_session(self, username, password)
130 133 else:
131 134 session = login_user_session(self)
132 135
133 136 self.rc_login_data['csrf_token'] = auth.get_csrf_token(session)
134 137 self.rc_current_session = session
135 138 return session['rhodecode_user']
136 139
137 140 @property
138 141 def csrf_token(self):
139 142 return self.rc_login_data['csrf_token']
140 143
141 144 @property
142 145 def _pyramid_registry(self):
143 146 return self.app.config.registry
144 147
145 148 @property
146 149 def _pyramid_settings(self):
147 150 return self._pyramid_registry.settings
148 151
149 152
150 153 def set_anonymous_access(enabled):
151 154 """(Dis)allows anonymous access depending on parameter `enabled`"""
152 155 user = User.get_default_user()
153 156 user.active = enabled
154 157 Session().add(user)
155 158 Session().commit()
156 159 time.sleep(1.5) # must sleep for cache (1s to expire)
157 160 log.info('anonymous access is now: %s', enabled)
158 161 assert enabled == User.get_default_user().active, (
159 162 'Cannot set anonymous access')
160 163
161 164
162 165 def check_xfail_backends(node, backend_alias):
163 166 # Using "xfail_backends" here intentionally, since this marks work
164 167 # which is "to be done" soon.
165 168 skip_marker = node.get_closest_marker('xfail_backends')
166 169 if skip_marker and backend_alias in skip_marker.args:
167 170 msg = "Support for backend %s to be developed." % (backend_alias, )
168 171 msg = skip_marker.kwargs.get('reason', msg)
169 172 pytest.xfail(msg)
170 173
171 174
172 175 def check_skip_backends(node, backend_alias):
173 176 # Using "skip_backends" here intentionally, since this marks work which is
174 177 # not supported.
175 178 skip_marker = node.get_closest_marker('skip_backends')
176 179 if skip_marker and backend_alias in skip_marker.args:
177 180 msg = "Feature not supported for backend %s." % (backend_alias, )
178 181 msg = skip_marker.kwargs.get('reason', msg)
179 182 pytest.skip(msg)
180 183
181 184
182 185 def extract_git_repo_from_dump(dump_name, repo_name):
183 186 """Create git repo `repo_name` from dump `dump_name`."""
184 187 repos_path = ScmModel().repos_path
185 188 target_path = os.path.join(repos_path, repo_name)
186 189 rc_testdata.extract_git_dump(dump_name, target_path)
187 190 return target_path
188 191
189 192
190 193 def extract_hg_repo_from_dump(dump_name, repo_name):
191 194 """Create hg repo `repo_name` from dump `dump_name`."""
192 195 repos_path = ScmModel().repos_path
193 196 target_path = os.path.join(repos_path, repo_name)
194 197 rc_testdata.extract_hg_dump(dump_name, target_path)
195 198 return target_path
196 199
197 200
198 201 def extract_svn_repo_from_dump(dump_name, repo_name):
199 202 """Create a svn repo `repo_name` from dump `dump_name`."""
200 203 repos_path = ScmModel().repos_path
201 204 target_path = os.path.join(repos_path, repo_name)
202 205 SubversionRepository(target_path, create=True)
203 206 _load_svn_dump_into_repo(dump_name, target_path)
204 207 return target_path
205 208
206 209
207 210 def assert_message_in_log(log_records, message, levelno, module):
208 211 messages = [
209 212 r.message for r in log_records
210 213 if r.module == module and r.levelno == levelno
211 214 ]
212 215 assert message in messages
213 216
214 217
215 218 def _load_svn_dump_into_repo(dump_name, repo_path):
216 219 """
217 220 Utility to populate a svn repository with a named dump
218 221
219 222 Currently the dumps are in rc_testdata. They might later on be
220 223 integrated with the main repository once they stabilize more.
221 224 """
222 225 dump = rc_testdata.load_svn_dump(dump_name)
223 226 load_dump = subprocess32.Popen(
224 227 ['svnadmin', 'load', repo_path],
225 228 stdin=subprocess32.PIPE, stdout=subprocess32.PIPE,
226 229 stderr=subprocess32.PIPE)
227 230 out, err = load_dump.communicate(dump)
228 231 if load_dump.returncode != 0:
229 232 log.error("Output of load_dump command: %s", out)
230 233 log.error("Error output of load_dump command: %s", err)
231 234 raise Exception(
232 235 'Failed to load dump "%s" into repository at path "%s".'
233 236 % (dump_name, repo_path))
234 237
235 238
236 239 class AssertResponse(object):
237 240 """
238 241 Utility that helps to assert things about a given HTML response.
239 242 """
240 243
241 244 def __init__(self, response):
242 245 self.response = response
243 246
244 247 def get_imports(self):
245 248 return fromstring, tostring, CSSSelector
246 249
247 250 def one_element_exists(self, css_selector):
248 251 self.get_element(css_selector)
249 252
250 253 def no_element_exists(self, css_selector):
251 254 assert not self._get_elements(css_selector)
252 255
253 256 def element_equals_to(self, css_selector, expected_content):
254 257 element = self.get_element(css_selector)
255 258 element_text = self._element_to_string(element)
256 259 assert expected_content in element_text
257 260
258 261 def element_contains(self, css_selector, expected_content):
259 262 element = self.get_element(css_selector)
260 263 assert expected_content in element.text_content()
261 264
262 265 def element_value_contains(self, css_selector, expected_content):
263 266 element = self.get_element(css_selector)
264 267 assert expected_content in element.value
265 268
266 269 def contains_one_link(self, link_text, href):
267 270 fromstring, tostring, CSSSelector = self.get_imports()
268 271 doc = fromstring(self.response.body)
269 272 sel = CSSSelector('a[href]')
270 273 elements = [
271 274 e for e in sel(doc) if e.text_content().strip() == link_text]
272 275 assert len(elements) == 1, "Did not find link or found multiple links"
273 276 self._ensure_url_equal(elements[0].attrib.get('href'), href)
274 277
275 278 def contains_one_anchor(self, anchor_id):
276 279 fromstring, tostring, CSSSelector = self.get_imports()
277 280 doc = fromstring(self.response.body)
278 281 sel = CSSSelector('#' + anchor_id)
279 282 elements = sel(doc)
280 283 assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id)
281 284
282 285 def _ensure_url_equal(self, found, expected):
283 286 assert _Url(found) == _Url(expected)
284 287
285 288 def get_element(self, css_selector):
286 289 elements = self._get_elements(css_selector)
287 290 assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector)
288 291 return elements[0]
289 292
290 293 def get_elements(self, css_selector):
291 294 return self._get_elements(css_selector)
292 295
293 296 def _get_elements(self, css_selector):
294 297 fromstring, tostring, CSSSelector = self.get_imports()
295 298 doc = fromstring(self.response.body)
296 299 sel = CSSSelector(css_selector)
297 300 elements = sel(doc)
298 301 return elements
299 302
300 303 def _element_to_string(self, element):
301 304 fromstring, tostring, CSSSelector = self.get_imports()
302 305 return tostring(element)
303 306
304 307
305 308 class _Url(object):
306 309 """
307 310 A url object that can be compared with other url orbjects
308 311 without regard to the vagaries of encoding, escaping, and ordering
309 312 of parameters in query strings.
310 313
311 314 Inspired by
312 315 http://stackoverflow.com/questions/5371992/comparing-two-urls-in-python
313 316 """
314 317
315 318 def __init__(self, url):
316 319 parts = urlparse(url)
317 320 _query = frozenset(parse_qsl(parts.query))
318 321 _path = unquote_plus(parts.path)
319 322 parts = parts._replace(query=_query, path=_path)
320 323 self.parts = parts
321 324
322 325 def __eq__(self, other):
323 326 return self.parts == other.parts
324 327
325 328 def __hash__(self):
326 329 return hash(self.parts)
327 330
328 331
329 332 def run_test_concurrently(times, raise_catched_exc=True):
330 333 """
331 334 Add this decorator to small pieces of code that you want to test
332 335 concurrently
333 336
334 337 ex:
335 338
336 339 @test_concurrently(25)
337 340 def my_test_function():
338 341 ...
339 342 """
340 343 def test_concurrently_decorator(test_func):
341 344 def wrapper(*args, **kwargs):
342 345 exceptions = []
343 346
344 347 def call_test_func():
345 348 try:
346 349 test_func(*args, **kwargs)
347 350 except Exception as e:
348 351 exceptions.append(e)
349 352 if raise_catched_exc:
350 353 raise
351 354 threads = []
352 355 for i in range(times):
353 356 threads.append(threading.Thread(target=call_test_func))
354 357 for t in threads:
355 358 t.start()
356 359 for t in threads:
357 360 t.join()
358 361 if exceptions:
359 362 raise Exception(
360 363 'test_concurrently intercepted %s exceptions: %s' % (
361 364 len(exceptions), exceptions))
362 365 return wrapper
363 366 return test_concurrently_decorator
364 367
365 368
366 369 def wait_for_url(url, timeout=10):
367 370 """
368 371 Wait until URL becomes reachable.
369 372
370 373 It polls the URL until the timeout is reached or it became reachable.
371 374 If will call to `py.test.fail` in case the URL is not reachable.
372 375 """
373 376 timeout = time.time() + timeout
374 377 last = 0
375 378 wait = 0.1
376 379
377 380 while timeout > last:
378 381 last = time.time()
379 382 if is_url_reachable(url):
380 383 break
381 384 elif (last + wait) > time.time():
382 385 # Go to sleep because not enough time has passed since last check.
383 386 time.sleep(wait)
384 387 else:
385 388 pytest.fail("Timeout while waiting for URL {}".format(url))
386 389
387 390
388 391 def is_url_reachable(url):
389 392 try:
390 393 urllib2.urlopen(url)
391 394 except urllib2.URLError:
392 395 return False
393 396 return True
394 397
395 398
396 399 def repo_on_filesystem(repo_name):
397 400 from rhodecode.lib import vcs
398 401 from rhodecode.tests import TESTS_TMP_PATH
399 402 repo = vcs.get_vcs_instance(
400 403 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
401 404 return repo is not None
402 405
403 406
404 407 def commit_change(
405 408 repo, filename, content, message, vcs_type, parent=None, newfile=False):
406 409 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
407 410
408 411 repo = Repository.get_by_repo_name(repo)
409 412 _commit = parent
410 413 if not parent:
411 414 _commit = EmptyCommit(alias=vcs_type)
412 415
413 416 if newfile:
414 417 nodes = {
415 418 filename: {
416 419 'content': content
417 420 }
418 421 }
419 422 commit = ScmModel().create_nodes(
420 423 user=TEST_USER_ADMIN_LOGIN, repo=repo,
421 424 message=message,
422 425 nodes=nodes,
423 426 parent_commit=_commit,
424 427 author=TEST_USER_ADMIN_LOGIN,
425 428 )
426 429 else:
427 430 commit = ScmModel().commit_change(
428 431 repo=repo.scm_instance(), repo_name=repo.repo_name,
429 432 commit=parent, user=TEST_USER_ADMIN_LOGIN,
430 433 author=TEST_USER_ADMIN_LOGIN,
431 434 message=message,
432 435 content=content,
433 436 f_path=filename
434 437 )
435 438 return commit
436 439
437 440
438 441 def permission_update_data_generator(csrf_token, default=None, grant=None, revoke=None):
439 442 if not default:
440 443 raise ValueError('Permission for default user must be given')
441 444 form_data = [(
442 445 'csrf_token', csrf_token
443 446 )]
444 447 # add default
445 448 form_data.extend([
446 449 ('u_perm_1', default)
447 450 ])
448 451
449 452 if grant:
450 453 for cnt, (obj_id, perm, obj_name, obj_type) in enumerate(grant, 1):
451 454 form_data.extend([
452 455 ('perm_new_member_perm_new{}'.format(cnt), perm),
453 456 ('perm_new_member_id_new{}'.format(cnt), obj_id),
454 457 ('perm_new_member_name_new{}'.format(cnt), obj_name),
455 458 ('perm_new_member_type_new{}'.format(cnt), obj_type),
456 459
457 460 ])
458 461 if revoke:
459 462 for obj_id, obj_type in revoke:
460 463 form_data.extend([
461 464 ('perm_del_member_id_{}'.format(obj_id), obj_id),
462 465 ('perm_del_member_type_{}'.format(obj_id), obj_type),
463 466 ])
464 467 return form_data
General Comments 0
You need to be logged in to leave comments. Login now