##// END OF EJS Templates
added __eq__ operation on vcs Repository objects...
marcink -
r3692:5f9f4ece beta
parent child Browse files
Show More
@@ -1,997 +1,1001
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.base
4 4 ~~~~~~~~~~~~~~~~~
5 5
6 6 Base for all available scm backends
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import datetime
13 13 from itertools import chain
14 14 from rhodecode.lib.vcs.utils import author_name, author_email
15 15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 17 from rhodecode.lib.vcs.conf import settings
18 18
19 19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 22 RepositoryError
23 23
24 24
25 25 class BaseRepository(object):
26 26 """
27 27 Base Repository for final backends
28 28
29 29 **Attributes**
30 30
31 31 ``DEFAULT_BRANCH_NAME``
32 32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33 33
34 34 ``scm``
35 35 alias of scm, i.e. *git* or *hg*
36 36
37 37 ``repo``
38 38 object from external api
39 39
40 40 ``revisions``
41 41 list of all available revisions' ids, in ascending order
42 42
43 43 ``changesets``
44 44 storage dict caching returned changesets
45 45
46 46 ``path``
47 47 absolute path to the repository
48 48
49 49 ``branches``
50 50 branches as list of changesets
51 51
52 52 ``tags``
53 53 tags as list of changesets
54 54 """
55 55 scm = None
56 56 DEFAULT_BRANCH_NAME = None
57 57 EMPTY_CHANGESET = '0' * 40
58 58
59 59 def __init__(self, repo_path, create=False, **kwargs):
60 60 """
61 61 Initializes repository. Raises RepositoryError if repository could
62 62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 63 exists and ``create`` is set to True.
64 64
65 65 :param repo_path: local path of the repository
66 66 :param create=False: if set to True, would try to craete repository.
67 67 :param src_url=None: if set, should be proper url from which repository
68 68 would be cloned; requires ``create`` parameter to be set to True -
69 69 raises RepositoryError if src_url is set and create evaluates to
70 70 False
71 71 """
72 72 raise NotImplementedError
73 73
74 74 def __str__(self):
75 75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76 76
77 77 def __repr__(self):
78 78 return self.__str__()
79 79
80 80 def __len__(self):
81 81 return self.count()
82 82
83 def __eq__(self, other):
84 same_instance = isinstance(other, self.__class__)
85 return same_instance and getattr(other, 'path', None) == self.path
86
83 87 @LazyProperty
84 88 def alias(self):
85 89 for k, v in settings.BACKENDS.items():
86 90 if v.split('.')[-1] == str(self.__class__.__name__):
87 91 return k
88 92
89 93 @LazyProperty
90 94 def name(self):
91 95 raise NotImplementedError
92 96
93 97 @LazyProperty
94 98 def owner(self):
95 99 raise NotImplementedError
96 100
97 101 @LazyProperty
98 102 def description(self):
99 103 raise NotImplementedError
100 104
101 105 @LazyProperty
102 106 def size(self):
103 107 """
104 108 Returns combined size in bytes for all repository files
105 109 """
106 110
107 111 size = 0
108 112 try:
109 113 tip = self.get_changeset()
110 114 for topnode, dirs, files in tip.walk('/'):
111 115 for f in files:
112 116 size += tip.get_file_size(f.path)
113 117 for dir in dirs:
114 118 for f in files:
115 119 size += tip.get_file_size(f.path)
116 120
117 121 except RepositoryError, e:
118 122 pass
119 123 return size
120 124
121 125 def is_valid(self):
122 126 """
123 127 Validates repository.
124 128 """
125 129 raise NotImplementedError
126 130
127 131 def get_last_change(self):
128 132 self.get_changesets()
129 133
130 134 #==========================================================================
131 135 # CHANGESETS
132 136 #==========================================================================
133 137
134 138 def get_changeset(self, revision=None):
135 139 """
136 140 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 141 recent changeset is returned.
138 142
139 143 :raises ``EmptyRepositoryError``: if there are no revisions
140 144 """
141 145 raise NotImplementedError
142 146
143 147 def __iter__(self):
144 148 """
145 149 Allows Repository objects to be iterated.
146 150
147 151 *Requires* implementation of ``__getitem__`` method.
148 152 """
149 153 for revision in self.revisions:
150 154 yield self.get_changeset(revision)
151 155
152 156 def get_changesets(self, start=None, end=None, start_date=None,
153 157 end_date=None, branch_name=None, reverse=False):
154 158 """
155 159 Returns iterator of ``MercurialChangeset`` objects from start to end
156 160 not inclusive This should behave just like a list, ie. end is not
157 161 inclusive
158 162
159 163 :param start: None or str
160 164 :param end: None or str
161 165 :param start_date:
162 166 :param end_date:
163 167 :param branch_name:
164 168 :param reversed:
165 169 """
166 170 raise NotImplementedError
167 171
168 172 def __getslice__(self, i, j):
169 173 """
170 174 Returns a iterator of sliced repository
171 175 """
172 176 for rev in self.revisions[i:j]:
173 177 yield self.get_changeset(rev)
174 178
175 179 def __getitem__(self, key):
176 180 return self.get_changeset(key)
177 181
178 182 def count(self):
179 183 return len(self.revisions)
180 184
181 185 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 186 """
183 187 Creates and returns a tag for the given ``revision``.
184 188
185 189 :param name: name for new tag
186 190 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 191 :param revision: changeset id for which new tag would be created
188 192 :param message: message of the tag's commit
189 193 :param date: date of tag's commit
190 194
191 195 :raises TagAlreadyExistError: if tag with same name already exists
192 196 """
193 197 raise NotImplementedError
194 198
195 199 def remove_tag(self, name, user, message=None, date=None):
196 200 """
197 201 Removes tag with the given ``name``.
198 202
199 203 :param name: name of the tag to be removed
200 204 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 205 :param message: message of the tag's removal commit
202 206 :param date: date of tag's removal commit
203 207
204 208 :raises TagDoesNotExistError: if tag with given name does not exists
205 209 """
206 210 raise NotImplementedError
207 211
208 212 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 213 context=3):
210 214 """
211 215 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 216 ``rev2`` since ``rev1``.
213 217
214 218 :param rev1: Entry point from which diff is shown. Can be
215 219 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 220 the changes since empty state of the repository until ``rev2``
217 221 :param rev2: Until which revision changes should be shown.
218 222 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 223 changes. Defaults to ``False``.
220 224 :param context: How many lines before/after changed lines should be
221 225 shown. Defaults to ``3``.
222 226 """
223 227 raise NotImplementedError
224 228
225 229 # ========== #
226 230 # COMMIT API #
227 231 # ========== #
228 232
229 233 @LazyProperty
230 234 def in_memory_changeset(self):
231 235 """
232 236 Returns ``InMemoryChangeset`` object for this repository.
233 237 """
234 238 raise NotImplementedError
235 239
236 240 def add(self, filenode, **kwargs):
237 241 """
238 242 Commit api function that will add given ``FileNode`` into this
239 243 repository.
240 244
241 245 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 246 already in repository
243 247 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 248 *added*
245 249 """
246 250 raise NotImplementedError
247 251
248 252 def remove(self, filenode, **kwargs):
249 253 """
250 254 Commit api function that will remove given ``FileNode`` into this
251 255 repository.
252 256
253 257 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 258 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 259 """
256 260 raise NotImplementedError
257 261
258 262 def commit(self, message, **kwargs):
259 263 """
260 264 Persists current changes made on this repository and returns newly
261 265 created changeset.
262 266
263 267 :raises ``NothingChangedError``: if no changes has been made
264 268 """
265 269 raise NotImplementedError
266 270
267 271 def get_state(self):
268 272 """
269 273 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 274 containing ``FileNode`` objects.
271 275 """
272 276 raise NotImplementedError
273 277
274 278 def get_config_value(self, section, name, config_file=None):
275 279 """
276 280 Returns configuration value for a given [``section``] and ``name``.
277 281
278 282 :param section: Section we want to retrieve value from
279 283 :param name: Name of configuration we want to retrieve
280 284 :param config_file: A path to file which should be used to retrieve
281 285 configuration from (might also be a list of file paths)
282 286 """
283 287 raise NotImplementedError
284 288
285 289 def get_user_name(self, config_file=None):
286 290 """
287 291 Returns user's name from global configuration file.
288 292
289 293 :param config_file: A path to file which should be used to retrieve
290 294 configuration from (might also be a list of file paths)
291 295 """
292 296 raise NotImplementedError
293 297
294 298 def get_user_email(self, config_file=None):
295 299 """
296 300 Returns user's email from global configuration file.
297 301
298 302 :param config_file: A path to file which should be used to retrieve
299 303 configuration from (might also be a list of file paths)
300 304 """
301 305 raise NotImplementedError
302 306
303 307 # =========== #
304 308 # WORKDIR API #
305 309 # =========== #
306 310
307 311 @LazyProperty
308 312 def workdir(self):
309 313 """
310 314 Returns ``Workdir`` instance for this repository.
311 315 """
312 316 raise NotImplementedError
313 317
314 318
315 319 class BaseChangeset(object):
316 320 """
317 321 Each backend should implement it's changeset representation.
318 322
319 323 **Attributes**
320 324
321 325 ``repository``
322 326 repository object within which changeset exists
323 327
324 328 ``id``
325 329 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
326 330
327 331 ``raw_id``
328 332 raw changeset representation (i.e. full 40 length sha for git
329 333 backend)
330 334
331 335 ``short_id``
332 336 shortened (if apply) version of ``raw_id``; it would be simple
333 337 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
334 338 as ``raw_id`` for subversion
335 339
336 340 ``revision``
337 341 revision number as integer
338 342
339 343 ``files``
340 344 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
341 345
342 346 ``dirs``
343 347 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
344 348
345 349 ``nodes``
346 350 combined list of ``Node`` objects
347 351
348 352 ``author``
349 353 author of the changeset, as unicode
350 354
351 355 ``message``
352 356 message of the changeset, as unicode
353 357
354 358 ``parents``
355 359 list of parent changesets
356 360
357 361 ``last``
358 362 ``True`` if this is last changeset in repository, ``False``
359 363 otherwise; trying to access this attribute while there is no
360 364 changesets would raise ``EmptyRepositoryError``
361 365 """
362 366 def __str__(self):
363 367 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
364 368 self.short_id)
365 369
366 370 def __repr__(self):
367 371 return self.__str__()
368 372
369 373 def __unicode__(self):
370 374 return u'%s:%s' % (self.revision, self.short_id)
371 375
372 376 def __eq__(self, other):
373 377 return self.raw_id == other.raw_id
374 378
375 379 def __json__(self):
376 380 return dict(
377 381 short_id=self.short_id,
378 382 raw_id=self.raw_id,
379 383 revision=self.revision,
380 384 message=self.message,
381 385 date=self.date,
382 386 author=self.author,
383 387 )
384 388
385 389 @LazyProperty
386 390 def last(self):
387 391 if self.repository is None:
388 392 raise ChangesetError("Cannot check if it's most recent revision")
389 393 return self.raw_id == self.repository.revisions[-1]
390 394
391 395 @LazyProperty
392 396 def parents(self):
393 397 """
394 398 Returns list of parents changesets.
395 399 """
396 400 raise NotImplementedError
397 401
398 402 @LazyProperty
399 403 def children(self):
400 404 """
401 405 Returns list of children changesets.
402 406 """
403 407 raise NotImplementedError
404 408
405 409 @LazyProperty
406 410 def id(self):
407 411 """
408 412 Returns string identifying this changeset.
409 413 """
410 414 raise NotImplementedError
411 415
412 416 @LazyProperty
413 417 def raw_id(self):
414 418 """
415 419 Returns raw string identifying this changeset.
416 420 """
417 421 raise NotImplementedError
418 422
419 423 @LazyProperty
420 424 def short_id(self):
421 425 """
422 426 Returns shortened version of ``raw_id`` attribute, as string,
423 427 identifying this changeset, useful for web representation.
424 428 """
425 429 raise NotImplementedError
426 430
427 431 @LazyProperty
428 432 def revision(self):
429 433 """
430 434 Returns integer identifying this changeset.
431 435
432 436 """
433 437 raise NotImplementedError
434 438
435 439 @LazyProperty
436 440 def committer(self):
437 441 """
438 442 Returns Committer for given commit
439 443 """
440 444
441 445 raise NotImplementedError
442 446
443 447 @LazyProperty
444 448 def committer_name(self):
445 449 """
446 450 Returns Author name for given commit
447 451 """
448 452
449 453 return author_name(self.committer)
450 454
451 455 @LazyProperty
452 456 def committer_email(self):
453 457 """
454 458 Returns Author email address for given commit
455 459 """
456 460
457 461 return author_email(self.committer)
458 462
459 463 @LazyProperty
460 464 def author(self):
461 465 """
462 466 Returns Author for given commit
463 467 """
464 468
465 469 raise NotImplementedError
466 470
467 471 @LazyProperty
468 472 def author_name(self):
469 473 """
470 474 Returns Author name for given commit
471 475 """
472 476
473 477 return author_name(self.author)
474 478
475 479 @LazyProperty
476 480 def author_email(self):
477 481 """
478 482 Returns Author email address for given commit
479 483 """
480 484
481 485 return author_email(self.author)
482 486
483 487 def get_file_mode(self, path):
484 488 """
485 489 Returns stat mode of the file at the given ``path``.
486 490 """
487 491 raise NotImplementedError
488 492
489 493 def get_file_content(self, path):
490 494 """
491 495 Returns content of the file at the given ``path``.
492 496 """
493 497 raise NotImplementedError
494 498
495 499 def get_file_size(self, path):
496 500 """
497 501 Returns size of the file at the given ``path``.
498 502 """
499 503 raise NotImplementedError
500 504
501 505 def get_file_changeset(self, path):
502 506 """
503 507 Returns last commit of the file at the given ``path``.
504 508 """
505 509 raise NotImplementedError
506 510
507 511 def get_file_history(self, path):
508 512 """
509 513 Returns history of file as reversed list of ``Changeset`` objects for
510 514 which file at given ``path`` has been modified.
511 515 """
512 516 raise NotImplementedError
513 517
514 518 def get_nodes(self, path):
515 519 """
516 520 Returns combined ``DirNode`` and ``FileNode`` objects list representing
517 521 state of changeset at the given ``path``.
518 522
519 523 :raises ``ChangesetError``: if node at the given ``path`` is not
520 524 instance of ``DirNode``
521 525 """
522 526 raise NotImplementedError
523 527
524 528 def get_node(self, path):
525 529 """
526 530 Returns ``Node`` object from the given ``path``.
527 531
528 532 :raises ``NodeDoesNotExistError``: if there is no node at the given
529 533 ``path``
530 534 """
531 535 raise NotImplementedError
532 536
533 537 def fill_archive(self, stream=None, kind='tgz', prefix=None):
534 538 """
535 539 Fills up given stream.
536 540
537 541 :param stream: file like object.
538 542 :param kind: one of following: ``zip``, ``tar``, ``tgz``
539 543 or ``tbz2``. Default: ``tgz``.
540 544 :param prefix: name of root directory in archive.
541 545 Default is repository name and changeset's raw_id joined with dash.
542 546
543 547 repo-tip.<kind>
544 548 """
545 549
546 550 raise NotImplementedError
547 551
548 552 def get_chunked_archive(self, **kwargs):
549 553 """
550 554 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
551 555
552 556 :param chunk_size: extra parameter which controls size of returned
553 557 chunks. Default:8k.
554 558 """
555 559
556 560 chunk_size = kwargs.pop('chunk_size', 8192)
557 561 stream = kwargs.get('stream')
558 562 self.fill_archive(**kwargs)
559 563 while True:
560 564 data = stream.read(chunk_size)
561 565 if not data:
562 566 break
563 567 yield data
564 568
565 569 @LazyProperty
566 570 def root(self):
567 571 """
568 572 Returns ``RootNode`` object for this changeset.
569 573 """
570 574 return self.get_node('')
571 575
572 576 def next(self, branch=None):
573 577 """
574 578 Returns next changeset from current, if branch is gives it will return
575 579 next changeset belonging to this branch
576 580
577 581 :param branch: show changesets within the given named branch
578 582 """
579 583 raise NotImplementedError
580 584
581 585 def prev(self, branch=None):
582 586 """
583 587 Returns previous changeset from current, if branch is gives it will
584 588 return previous changeset belonging to this branch
585 589
586 590 :param branch: show changesets within the given named branch
587 591 """
588 592 raise NotImplementedError
589 593
590 594 @LazyProperty
591 595 def added(self):
592 596 """
593 597 Returns list of added ``FileNode`` objects.
594 598 """
595 599 raise NotImplementedError
596 600
597 601 @LazyProperty
598 602 def changed(self):
599 603 """
600 604 Returns list of modified ``FileNode`` objects.
601 605 """
602 606 raise NotImplementedError
603 607
604 608 @LazyProperty
605 609 def removed(self):
606 610 """
607 611 Returns list of removed ``FileNode`` objects.
608 612 """
609 613 raise NotImplementedError
610 614
611 615 @LazyProperty
612 616 def size(self):
613 617 """
614 618 Returns total number of bytes from contents of all filenodes.
615 619 """
616 620 return sum((node.size for node in self.get_filenodes_generator()))
617 621
618 622 def walk(self, topurl=''):
619 623 """
620 624 Similar to os.walk method. Insted of filesystem it walks through
621 625 changeset starting at given ``topurl``. Returns generator of tuples
622 626 (topnode, dirnodes, filenodes).
623 627 """
624 628 topnode = self.get_node(topurl)
625 629 yield (topnode, topnode.dirs, topnode.files)
626 630 for dirnode in topnode.dirs:
627 631 for tup in self.walk(dirnode.path):
628 632 yield tup
629 633
630 634 def get_filenodes_generator(self):
631 635 """
632 636 Returns generator that yields *all* file nodes.
633 637 """
634 638 for topnode, dirs, files in self.walk():
635 639 for node in files:
636 640 yield node
637 641
638 642 def as_dict(self):
639 643 """
640 644 Returns dictionary with changeset's attributes and their values.
641 645 """
642 646 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
643 647 'revision', 'date', 'message'])
644 648 data['author'] = {'name': self.author_name, 'email': self.author_email}
645 649 data['added'] = [node.path for node in self.added]
646 650 data['changed'] = [node.path for node in self.changed]
647 651 data['removed'] = [node.path for node in self.removed]
648 652 return data
649 653
650 654
651 655 class BaseWorkdir(object):
652 656 """
653 657 Working directory representation of single repository.
654 658
655 659 :attribute: repository: repository object of working directory
656 660 """
657 661
658 662 def __init__(self, repository):
659 663 self.repository = repository
660 664
661 665 def get_branch(self):
662 666 """
663 667 Returns name of current branch.
664 668 """
665 669 raise NotImplementedError
666 670
667 671 def get_changeset(self):
668 672 """
669 673 Returns current changeset.
670 674 """
671 675 raise NotImplementedError
672 676
673 677 def get_added(self):
674 678 """
675 679 Returns list of ``FileNode`` objects marked as *new* in working
676 680 directory.
677 681 """
678 682 raise NotImplementedError
679 683
680 684 def get_changed(self):
681 685 """
682 686 Returns list of ``FileNode`` objects *changed* in working directory.
683 687 """
684 688 raise NotImplementedError
685 689
686 690 def get_removed(self):
687 691 """
688 692 Returns list of ``RemovedFileNode`` objects marked as *removed* in
689 693 working directory.
690 694 """
691 695 raise NotImplementedError
692 696
693 697 def get_untracked(self):
694 698 """
695 699 Returns list of ``FileNode`` objects which are present within working
696 700 directory however are not tracked by repository.
697 701 """
698 702 raise NotImplementedError
699 703
700 704 def get_status(self):
701 705 """
702 706 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
703 707 lists.
704 708 """
705 709 raise NotImplementedError
706 710
707 711 def commit(self, message, **kwargs):
708 712 """
709 713 Commits local (from working directory) changes and returns newly
710 714 created
711 715 ``Changeset``. Updates repository's ``revisions`` list.
712 716
713 717 :raises ``CommitError``: if any error occurs while committing
714 718 """
715 719 raise NotImplementedError
716 720
717 721 def update(self, revision=None):
718 722 """
719 723 Fetches content of the given revision and populates it within working
720 724 directory.
721 725 """
722 726 raise NotImplementedError
723 727
724 728 def checkout_branch(self, branch=None):
725 729 """
726 730 Checks out ``branch`` or the backend's default branch.
727 731
728 732 Raises ``BranchDoesNotExistError`` if the branch does not exist.
729 733 """
730 734 raise NotImplementedError
731 735
732 736
733 737 class BaseInMemoryChangeset(object):
734 738 """
735 739 Represents differences between repository's state (most recent head) and
736 740 changes made *in place*.
737 741
738 742 **Attributes**
739 743
740 744 ``repository``
741 745 repository object for this in-memory-changeset
742 746
743 747 ``added``
744 748 list of ``FileNode`` objects marked as *added*
745 749
746 750 ``changed``
747 751 list of ``FileNode`` objects marked as *changed*
748 752
749 753 ``removed``
750 754 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
751 755 *removed*
752 756
753 757 ``parents``
754 758 list of ``Changeset`` representing parents of in-memory changeset.
755 759 Should always be 2-element sequence.
756 760
757 761 """
758 762
759 763 def __init__(self, repository):
760 764 self.repository = repository
761 765 self.added = []
762 766 self.changed = []
763 767 self.removed = []
764 768 self.parents = []
765 769
766 770 def add(self, *filenodes):
767 771 """
768 772 Marks given ``FileNode`` objects as *to be committed*.
769 773
770 774 :raises ``NodeAlreadyExistsError``: if node with same path exists at
771 775 latest changeset
772 776 :raises ``NodeAlreadyAddedError``: if node with same path is already
773 777 marked as *added*
774 778 """
775 779 # Check if not already marked as *added* first
776 780 for node in filenodes:
777 781 if node.path in (n.path for n in self.added):
778 782 raise NodeAlreadyAddedError("Such FileNode %s is already "
779 783 "marked for addition" % node.path)
780 784 for node in filenodes:
781 785 self.added.append(node)
782 786
783 787 def change(self, *filenodes):
784 788 """
785 789 Marks given ``FileNode`` objects to be *changed* in next commit.
786 790
787 791 :raises ``EmptyRepositoryError``: if there are no changesets yet
788 792 :raises ``NodeAlreadyExistsError``: if node with same path is already
789 793 marked to be *changed*
790 794 :raises ``NodeAlreadyRemovedError``: if node with same path is already
791 795 marked to be *removed*
792 796 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
793 797 changeset
794 798 :raises ``NodeNotChangedError``: if node hasn't really be changed
795 799 """
796 800 for node in filenodes:
797 801 if node.path in (n.path for n in self.removed):
798 802 raise NodeAlreadyRemovedError("Node at %s is already marked "
799 803 "as removed" % node.path)
800 804 try:
801 805 self.repository.get_changeset()
802 806 except EmptyRepositoryError:
803 807 raise EmptyRepositoryError("Nothing to change - try to *add* new "
804 808 "nodes rather than changing them")
805 809 for node in filenodes:
806 810 if node.path in (n.path for n in self.changed):
807 811 raise NodeAlreadyChangedError("Node at '%s' is already "
808 812 "marked as changed" % node.path)
809 813 self.changed.append(node)
810 814
811 815 def remove(self, *filenodes):
812 816 """
813 817 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
814 818 *removed* in next commit.
815 819
816 820 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
817 821 be *removed*
818 822 :raises ``NodeAlreadyChangedError``: if node has been already marked to
819 823 be *changed*
820 824 """
821 825 for node in filenodes:
822 826 if node.path in (n.path for n in self.removed):
823 827 raise NodeAlreadyRemovedError("Node is already marked to "
824 828 "for removal at %s" % node.path)
825 829 if node.path in (n.path for n in self.changed):
826 830 raise NodeAlreadyChangedError("Node is already marked to "
827 831 "be changed at %s" % node.path)
828 832 # We only mark node as *removed* - real removal is done by
829 833 # commit method
830 834 self.removed.append(node)
831 835
832 836 def reset(self):
833 837 """
834 838 Resets this instance to initial state (cleans ``added``, ``changed``
835 839 and ``removed`` lists).
836 840 """
837 841 self.added = []
838 842 self.changed = []
839 843 self.removed = []
840 844 self.parents = []
841 845
842 846 def get_ipaths(self):
843 847 """
844 848 Returns generator of paths from nodes marked as added, changed or
845 849 removed.
846 850 """
847 851 for node in chain(self.added, self.changed, self.removed):
848 852 yield node.path
849 853
850 854 def get_paths(self):
851 855 """
852 856 Returns list of paths from nodes marked as added, changed or removed.
853 857 """
854 858 return list(self.get_ipaths())
855 859
856 860 def check_integrity(self, parents=None):
857 861 """
858 862 Checks in-memory changeset's integrity. Also, sets parents if not
859 863 already set.
860 864
861 865 :raises CommitError: if any error occurs (i.e.
862 866 ``NodeDoesNotExistError``).
863 867 """
864 868 if not self.parents:
865 869 parents = parents or []
866 870 if len(parents) == 0:
867 871 try:
868 872 parents = [self.repository.get_changeset(), None]
869 873 except EmptyRepositoryError:
870 874 parents = [None, None]
871 875 elif len(parents) == 1:
872 876 parents += [None]
873 877 self.parents = parents
874 878
875 879 # Local parents, only if not None
876 880 parents = [p for p in self.parents if p]
877 881
878 882 # Check nodes marked as added
879 883 for p in parents:
880 884 for node in self.added:
881 885 try:
882 886 p.get_node(node.path)
883 887 except NodeDoesNotExistError:
884 888 pass
885 889 else:
886 890 raise NodeAlreadyExistsError("Node at %s already exists "
887 891 "at %s" % (node.path, p))
888 892
889 893 # Check nodes marked as changed
890 894 missing = set(self.changed)
891 895 not_changed = set(self.changed)
892 896 if self.changed and not parents:
893 897 raise NodeDoesNotExistError(str(self.changed[0].path))
894 898 for p in parents:
895 899 for node in self.changed:
896 900 try:
897 901 old = p.get_node(node.path)
898 902 missing.remove(node)
899 903 if old.content != node.content:
900 904 not_changed.remove(node)
901 905 except NodeDoesNotExistError:
902 906 pass
903 907 if self.changed and missing:
904 908 raise NodeDoesNotExistError("Node at %s is missing "
905 909 "(parents: %s)" % (node.path, parents))
906 910
907 911 if self.changed and not_changed:
908 912 raise NodeNotChangedError("Node at %s wasn't actually changed "
909 913 "since parents' changesets: %s" % (not_changed.pop().path,
910 914 parents)
911 915 )
912 916
913 917 # Check nodes marked as removed
914 918 if self.removed and not parents:
915 919 raise NodeDoesNotExistError("Cannot remove node at %s as there "
916 920 "were no parents specified" % self.removed[0].path)
917 921 really_removed = set()
918 922 for p in parents:
919 923 for node in self.removed:
920 924 try:
921 925 p.get_node(node.path)
922 926 really_removed.add(node)
923 927 except ChangesetError:
924 928 pass
925 929 not_removed = set(self.removed) - really_removed
926 930 if not_removed:
927 931 raise NodeDoesNotExistError("Cannot remove node at %s from "
928 932 "following parents: %s" % (not_removed[0], parents))
929 933
930 934 def commit(self, message, author, parents=None, branch=None, date=None,
931 935 **kwargs):
932 936 """
933 937 Performs in-memory commit (doesn't check workdir in any way) and
934 938 returns newly created ``Changeset``. Updates repository's
935 939 ``revisions``.
936 940
937 941 .. note::
938 942 While overriding this method each backend's should call
939 943 ``self.check_integrity(parents)`` in the first place.
940 944
941 945 :param message: message of the commit
942 946 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
943 947 :param parents: single parent or sequence of parents from which commit
944 948 would be derieved
945 949 :param date: ``datetime.datetime`` instance. Defaults to
946 950 ``datetime.datetime.now()``.
947 951 :param branch: branch name, as string. If none given, default backend's
948 952 branch would be used.
949 953
950 954 :raises ``CommitError``: if any error occurs while committing
951 955 """
952 956 raise NotImplementedError
953 957
954 958
955 959 class EmptyChangeset(BaseChangeset):
956 960 """
957 961 An dummy empty changeset. It's possible to pass hash when creating
958 962 an EmptyChangeset
959 963 """
960 964
961 965 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
962 966 alias=None, revision=-1, message='', author='', date=None):
963 967 self._empty_cs = cs
964 968 self.revision = revision
965 969 self.message = message
966 970 self.author = author
967 971 self.date = date or datetime.datetime.fromtimestamp(0)
968 972 self.repository = repo
969 973 self.requested_revision = requested_revision
970 974 self.alias = alias
971 975
972 976 @LazyProperty
973 977 def raw_id(self):
974 978 """
975 979 Returns raw string identifying this changeset, useful for web
976 980 representation.
977 981 """
978 982
979 983 return self._empty_cs
980 984
981 985 @LazyProperty
982 986 def branch(self):
983 987 from rhodecode.lib.vcs.backends import get_backend
984 988 return get_backend(self.alias).DEFAULT_BRANCH_NAME
985 989
986 990 @LazyProperty
987 991 def short_id(self):
988 992 return self.raw_id[:12]
989 993
990 994 def get_file_changeset(self, path):
991 995 return self
992 996
993 997 def get_file_content(self, path):
994 998 return u''
995 999
996 1000 def get_file_size(self, path):
997 1001 return 0
@@ -1,216 +1,229
1 1 from __future__ import with_statement
2 2 import datetime
3 3 from base import BackendTestMixin
4 4 from conf import SCM_TESTS
5 5 from conf import TEST_USER_CONFIG_FILE
6 6 from rhodecode.lib.vcs.nodes import FileNode
7 7 from rhodecode.lib.vcs.utils.compat import unittest
8 8 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
9 9
10 10
11 11 class RepositoryBaseTest(BackendTestMixin):
12 12 recreate_repo_per_test = False
13 13
14 14 @classmethod
15 15 def _get_commits(cls):
16 16 return super(RepositoryBaseTest, cls)._get_commits()[:1]
17 17
18 18 def test_get_config_value(self):
19 19 self.assertEqual(self.repo.get_config_value('universal', 'foo',
20 20 TEST_USER_CONFIG_FILE), 'bar')
21 21
22 22 def test_get_config_value_defaults_to_None(self):
23 23 self.assertEqual(self.repo.get_config_value('universal', 'nonexist',
24 24 TEST_USER_CONFIG_FILE), None)
25 25
26 26 def test_get_user_name(self):
27 27 self.assertEqual(self.repo.get_user_name(TEST_USER_CONFIG_FILE),
28 28 'Foo Bar')
29 29
30 30 def test_get_user_email(self):
31 31 self.assertEqual(self.repo.get_user_email(TEST_USER_CONFIG_FILE),
32 32 'foo.bar@example.com')
33 33
34 def test_repo_equality(self):
35 self.assertTrue(self.repo == self.repo)
36
37 def test_repo_equality_broken_object(self):
38 import copy
39 _repo = copy.copy(self.repo)
40 delattr(_repo, 'path')
41 self.assertTrue(self.repo != _repo)
42
43 def test_repo_equality_other_object(self):
44 class dummy(object):
45 path = self.repo.path
46 self.assertTrue(self.repo != dummy())
34 47
35 48
36 49 class RepositoryGetDiffTest(BackendTestMixin):
37 50
38 51 @classmethod
39 52 def _get_commits(cls):
40 53 commits = [
41 54 {
42 55 'message': 'Initial commit',
43 56 'author': 'Joe Doe <joe.doe@example.com>',
44 57 'date': datetime.datetime(2010, 1, 1, 20),
45 58 'added': [
46 59 FileNode('foobar', content='foobar'),
47 60 FileNode('foobar2', content='foobar2'),
48 61 ],
49 62 },
50 63 {
51 64 'message': 'Changed foobar, added foobar3',
52 65 'author': 'Jane Doe <jane.doe@example.com>',
53 66 'date': datetime.datetime(2010, 1, 1, 21),
54 67 'added': [
55 68 FileNode('foobar3', content='foobar3'),
56 69 ],
57 70 'changed': [
58 71 FileNode('foobar', 'FOOBAR'),
59 72 ],
60 73 },
61 74 {
62 75 'message': 'Removed foobar, changed foobar3',
63 76 'author': 'Jane Doe <jane.doe@example.com>',
64 77 'date': datetime.datetime(2010, 1, 1, 22),
65 78 'changed': [
66 79 FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'),
67 80 ],
68 81 'removed': [FileNode('foobar')],
69 82 },
70 83 ]
71 84 return commits
72 85
73 86 def test_raise_for_wrong(self):
74 87 with self.assertRaises(ChangesetDoesNotExistError):
75 88 self.repo.get_diff('a' * 40, 'b' * 40)
76 89
77 90
78 91 class GitRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase):
79 92 backend_alias = 'git'
80 93
81 94 def test_initial_commit_diff(self):
82 95 initial_rev = self.repo.revisions[0]
83 96 self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar
84 97 new file mode 100644
85 98 index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632
86 99 --- /dev/null
87 100 +++ b/foobar
88 101 @@ -0,0 +1 @@
89 102 +foobar
90 103 \ No newline at end of file
91 104 diff --git a/foobar2 b/foobar2
92 105 new file mode 100644
93 106 index 0000000000000000000000000000000000000000..e8c9d6b98e3dce993a464935e1a53f50b56a3783
94 107 --- /dev/null
95 108 +++ b/foobar2
96 109 @@ -0,0 +1 @@
97 110 +foobar2
98 111 \ No newline at end of file
99 112 ''')
100 113
101 114 def test_second_changeset_diff(self):
102 115 revs = self.repo.revisions
103 116 self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar
104 117 index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644
105 118 --- a/foobar
106 119 +++ b/foobar
107 120 @@ -1 +1 @@
108 121 -foobar
109 122 \ No newline at end of file
110 123 +FOOBAR
111 124 \ No newline at end of file
112 125 diff --git a/foobar3 b/foobar3
113 126 new file mode 100644
114 127 index 0000000000000000000000000000000000000000..c11c37d41d33fb47741cff93fa5f9d798c1535b0
115 128 --- /dev/null
116 129 +++ b/foobar3
117 130 @@ -0,0 +1 @@
118 131 +foobar3
119 132 \ No newline at end of file
120 133 ''')
121 134
122 135 def test_third_changeset_diff(self):
123 136 revs = self.repo.revisions
124 137 self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar
125 138 deleted file mode 100644
126 139 index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000
127 140 --- a/foobar
128 141 +++ /dev/null
129 142 @@ -1 +0,0 @@
130 143 -FOOBAR
131 144 \ No newline at end of file
132 145 diff --git a/foobar3 b/foobar3
133 146 index c11c37d41d33fb47741cff93fa5f9d798c1535b0..f9324477362684ff692aaf5b9a81e01b9e9a671c 100644
134 147 --- a/foobar3
135 148 +++ b/foobar3
136 149 @@ -1 +1,3 @@
137 150 -foobar3
138 151 \ No newline at end of file
139 152 +FOOBAR
140 153 +FOOBAR
141 154 +FOOBAR
142 155 ''')
143 156
144 157
145 158 class HgRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase):
146 159 backend_alias = 'hg'
147 160
148 161 def test_initial_commit_diff(self):
149 162 initial_rev = self.repo.revisions[0]
150 163 self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar
151 164 new file mode 100755
152 165 --- /dev/null
153 166 +++ b/foobar
154 167 @@ -0,0 +1,1 @@
155 168 +foobar
156 169 \ No newline at end of file
157 170 diff --git a/foobar2 b/foobar2
158 171 new file mode 100755
159 172 --- /dev/null
160 173 +++ b/foobar2
161 174 @@ -0,0 +1,1 @@
162 175 +foobar2
163 176 \ No newline at end of file
164 177 ''')
165 178
166 179 def test_second_changeset_diff(self):
167 180 revs = self.repo.revisions
168 181 self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar
169 182 --- a/foobar
170 183 +++ b/foobar
171 184 @@ -1,1 +1,1 @@
172 185 -foobar
173 186 \ No newline at end of file
174 187 +FOOBAR
175 188 \ No newline at end of file
176 189 diff --git a/foobar3 b/foobar3
177 190 new file mode 100755
178 191 --- /dev/null
179 192 +++ b/foobar3
180 193 @@ -0,0 +1,1 @@
181 194 +foobar3
182 195 \ No newline at end of file
183 196 ''')
184 197
185 198 def test_third_changeset_diff(self):
186 199 revs = self.repo.revisions
187 200 self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar
188 201 deleted file mode 100755
189 202 --- a/foobar
190 203 +++ /dev/null
191 204 @@ -1,1 +0,0 @@
192 205 -FOOBAR
193 206 \ No newline at end of file
194 207 diff --git a/foobar3 b/foobar3
195 208 --- a/foobar3
196 209 +++ b/foobar3
197 210 @@ -1,1 +1,3 @@
198 211 -foobar3
199 212 \ No newline at end of file
200 213 +FOOBAR
201 214 +FOOBAR
202 215 +FOOBAR
203 216 ''')
204 217
205 218
206 219 # For each backend create test case class
207 220 for alias in SCM_TESTS:
208 221 attrs = {
209 222 'backend_alias': alias,
210 223 }
211 224 cls_name = alias.capitalize() + RepositoryBaseTest.__name__
212 225 bases = (RepositoryBaseTest, unittest.TestCase)
213 226 globals()[cls_name] = type(cls_name, bases, attrs)
214 227
215 228 if __name__ == '__main__':
216 229 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now