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