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