##// END OF EJS Templates
vcs: use correct name of parameter “reverse” in docstring of get_changesets()
Manuel Jacob -
r8707:586ce8c2 stable
parent child Browse files
Show More
@@ -1,1075 +1,1075 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 from typing import Sequence
15 15
16 16 from kallithea.lib.vcs.backends import get_backend
17 17 from kallithea.lib.vcs.conf import settings
18 18 from kallithea.lib.vcs.exceptions import (ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError,
19 19 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, RepositoryError)
20 20 from kallithea.lib.vcs.utils import author_email, author_name
21 21 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
22 22 from kallithea.lib.vcs.utils.lazy import LazyProperty
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. "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 DEFAULT_BRANCH_NAME: str # assigned in subclass
56 56 scm: str # assigned in subclass
57 57 path: str # assigned in subclass __init__
58 58 revisions: Sequence[str] # LazyProperty in subclass
59 59 _empty: bool # property in subclass
60 60
61 61 EMPTY_CHANGESET = '0' * 40
62 62
63 63 def __init__(self, repo_path, create=False, **kwargs):
64 64 """
65 65 Initializes repository. Raises RepositoryError if repository could
66 66 not be find at the given ``repo_path`` or directory at ``repo_path``
67 67 exists and ``create`` is set to True.
68 68
69 69 :param repo_path: local path of the repository
70 70 :param create=False: if set to True, would try to create repository.
71 71 :param src_url=None: if set, should be proper url from which repository
72 72 would be cloned; requires ``create`` parameter to be set to True -
73 73 raises RepositoryError if src_url is set and create evaluates to
74 74 False
75 75 """
76 76 raise NotImplementedError
77 77
78 78 def __str__(self):
79 79 return '<%s at %s>' % (self.__class__.__name__, self.path)
80 80
81 81 def __repr__(self):
82 82 return self.__str__()
83 83
84 84 def __len__(self):
85 85 return self.count()
86 86
87 87 def __eq__(self, other):
88 88 same_instance = isinstance(other, self.__class__)
89 89 return same_instance and getattr(other, 'path', None) == self.path
90 90
91 91 def __ne__(self, other):
92 92 return not self.__eq__(other)
93 93
94 94 @LazyProperty
95 95 def alias(self):
96 96 for k, v in settings.BACKENDS.items():
97 97 if v.split('.')[-1] == str(self.__class__.__name__):
98 98 return k
99 99
100 100 @LazyProperty
101 101 def name(self):
102 102 """
103 103 Return repository name (without group name)
104 104 """
105 105 raise NotImplementedError
106 106
107 107 @LazyProperty
108 108 def owner(self):
109 109 raise NotImplementedError
110 110
111 111 @LazyProperty
112 112 def description(self):
113 113 raise NotImplementedError
114 114
115 115 @LazyProperty
116 116 def size(self):
117 117 """
118 118 Returns combined size in bytes for all repository files
119 119 """
120 120
121 121 size = 0
122 122 try:
123 123 tip = self.get_changeset()
124 124 for topnode, dirs, files in tip.walk('/'):
125 125 for f in files:
126 126 size += tip.get_file_size(f.path)
127 127
128 128 except RepositoryError as e:
129 129 pass
130 130 return size
131 131
132 132 def is_valid(self):
133 133 """
134 134 Validates repository.
135 135 """
136 136 raise NotImplementedError
137 137
138 138 def is_empty(self):
139 139 return self._empty
140 140
141 141 #==========================================================================
142 142 # CHANGESETS
143 143 #==========================================================================
144 144
145 145 def get_changeset(self, revision=None):
146 146 """
147 147 Returns instance of ``Changeset`` class. If ``revision`` is None, most
148 148 recent changeset is returned.
149 149
150 150 :raises ``EmptyRepositoryError``: if there are no revisions
151 151 """
152 152 raise NotImplementedError
153 153
154 154 def __iter__(self):
155 155 """
156 156 Allows Repository objects to be iterated.
157 157
158 158 *Requires* implementation of ``__getitem__`` method.
159 159 """
160 160 for revision in self.revisions:
161 161 yield self.get_changeset(revision)
162 162
163 163 def get_changesets(self, start=None, end=None, start_date=None,
164 164 end_date=None, branch_name=None, reverse=False, max_revisions=None):
165 165 """
166 166 Returns iterator of ``BaseChangeset`` objects from start to end,
167 167 both inclusive.
168 168
169 169 :param start: None or str
170 170 :param end: None or str
171 171 :param start_date:
172 172 :param end_date:
173 173 :param branch_name:
174 :param reversed:
174 :param reverse:
175 175 """
176 176 raise NotImplementedError
177 177
178 178 def get_diff_changesets(self, org_rev, other_repo, other_rev):
179 179 """
180 180 Returns lists of changesets that can be merged from this repo @org_rev
181 181 to other_repo @other_rev
182 182 ... and the other way
183 183 ... and the ancestors that would be used for merge
184 184
185 185 :param org_rev: the revision we want our compare to be made
186 186 :param other_repo: repo object, most likely the fork of org_repo. It has
187 187 all changesets that we need to obtain
188 188 :param other_rev: revision we want out compare to be made on other_repo
189 189 """
190 190 raise NotImplementedError
191 191
192 192 def __getitem__(self, key):
193 193 if isinstance(key, slice):
194 194 return (self.get_changeset(rev) for rev in self.revisions[key])
195 195 return self.get_changeset(key)
196 196
197 197 def count(self):
198 198 return len(self.revisions)
199 199
200 200 def tag(self, name, user, revision=None, message=None, date=None, **opts):
201 201 """
202 202 Creates and returns a tag for the given ``revision``.
203 203
204 204 :param name: name for new tag
205 205 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
206 206 :param revision: changeset id for which new tag would be created
207 207 :param message: message of the tag's commit
208 208 :param date: date of tag's commit
209 209
210 210 :raises TagAlreadyExistError: if tag with same name already exists
211 211 """
212 212 raise NotImplementedError
213 213
214 214 def remove_tag(self, name, user, message=None, date=None):
215 215 """
216 216 Removes tag with the given ``name``.
217 217
218 218 :param name: name of the tag to be removed
219 219 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
220 220 :param message: message of the tag's removal commit
221 221 :param date: date of tag's removal commit
222 222
223 223 :raises TagDoesNotExistError: if tag with given name does not exists
224 224 """
225 225 raise NotImplementedError
226 226
227 227 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
228 228 context=3):
229 229 """
230 230 Returns (git like) *diff*, as plain text. Shows changes introduced by
231 231 ``rev2`` since ``rev1``.
232 232
233 233 :param rev1: Entry point from which diff is shown. Can be
234 234 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
235 235 the changes since empty state of the repository until ``rev2``
236 236 :param rev2: Until which revision changes should be shown.
237 237 :param ignore_whitespace: If set to ``True``, would not show whitespace
238 238 changes. Defaults to ``False``.
239 239 :param context: How many lines before/after changed lines should be
240 240 shown. Defaults to ``3``.
241 241 """
242 242 raise NotImplementedError
243 243
244 244 # ========== #
245 245 # COMMIT API #
246 246 # ========== #
247 247
248 248 @LazyProperty
249 249 def in_memory_changeset(self):
250 250 """
251 251 Returns ``InMemoryChangeset`` object for this repository.
252 252 """
253 253 raise NotImplementedError
254 254
255 255 def add(self, filenode, **kwargs):
256 256 """
257 257 Commit api function that will add given ``FileNode`` into this
258 258 repository.
259 259
260 260 :raises ``NodeAlreadyExistsError``: if there is a file with same path
261 261 already in repository
262 262 :raises ``NodeAlreadyAddedError``: if given node is already marked as
263 263 *added*
264 264 """
265 265 raise NotImplementedError
266 266
267 267 def remove(self, filenode, **kwargs):
268 268 """
269 269 Commit api function that will remove given ``FileNode`` into this
270 270 repository.
271 271
272 272 :raises ``EmptyRepositoryError``: if there are no changesets yet
273 273 :raises ``NodeDoesNotExistError``: if there is no file with given path
274 274 """
275 275 raise NotImplementedError
276 276
277 277 def commit(self, message, **kwargs):
278 278 """
279 279 Persists current changes made on this repository and returns newly
280 280 created changeset.
281 281 """
282 282 raise NotImplementedError
283 283
284 284 def get_state(self):
285 285 """
286 286 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
287 287 containing ``FileNode`` objects.
288 288 """
289 289 raise NotImplementedError
290 290
291 291 def get_config_value(self, section, name, config_file=None):
292 292 """
293 293 Returns configuration value for a given [``section``] and ``name``.
294 294
295 295 :param section: Section we want to retrieve value from
296 296 :param name: Name of configuration we want to retrieve
297 297 :param config_file: A path to file which should be used to retrieve
298 298 configuration from (might also be a list of file paths)
299 299 """
300 300 raise NotImplementedError
301 301
302 302 def get_user_name(self, config_file=None):
303 303 """
304 304 Returns user's name from global configuration file.
305 305
306 306 :param config_file: A path to file which should be used to retrieve
307 307 configuration from (might also be a list of file paths)
308 308 """
309 309 raise NotImplementedError
310 310
311 311 def get_user_email(self, config_file=None):
312 312 """
313 313 Returns user's email from global configuration file.
314 314
315 315 :param config_file: A path to file which should be used to retrieve
316 316 configuration from (might also be a list of file paths)
317 317 """
318 318 raise NotImplementedError
319 319
320 320 # =========== #
321 321 # WORKDIR API #
322 322 # =========== #
323 323
324 324 @LazyProperty
325 325 def workdir(self):
326 326 """
327 327 Returns ``Workdir`` instance for this repository.
328 328 """
329 329 raise NotImplementedError
330 330
331 331
332 332 class BaseChangeset(object):
333 333 """
334 334 Each backend should implement it's changeset representation.
335 335
336 336 **Attributes**
337 337
338 338 ``repository``
339 339 repository object within which changeset exists
340 340
341 341 ``raw_id``
342 342 raw changeset representation (i.e. full 40 length sha for git
343 343 backend)
344 344
345 345 ``short_id``
346 346 shortened (if apply) version of ``raw_id``; it would be simple
347 347 shortcut for ``raw_id[:12]`` for git/mercurial backends
348 348
349 349 ``revision``
350 350 revision number as integer
351 351
352 352 ``files``
353 353 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
354 354
355 355 ``dirs``
356 356 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
357 357
358 358 ``nodes``
359 359 combined list of ``Node`` objects
360 360
361 361 ``author``
362 362 author of the changeset, as str
363 363
364 364 ``message``
365 365 message of the changeset, as str
366 366
367 367 ``parents``
368 368 list of parent changesets
369 369
370 370 ``last``
371 371 ``True`` if this is last changeset in repository, ``False``
372 372 otherwise; trying to access this attribute while there is no
373 373 changesets would raise ``EmptyRepositoryError``
374 374 """
375 375 message: str # LazyProperty in subclass
376 376 date: datetime.datetime # LazyProperty in subclass
377 377
378 378 def __str__(self):
379 379 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
380 380 self.short_id)
381 381
382 382 def __repr__(self):
383 383 return self.__str__()
384 384
385 385 def __eq__(self, other):
386 386 if type(self) is not type(other):
387 387 return False
388 388 return self.raw_id == other.raw_id
389 389
390 390 def __json__(self, with_file_list=False):
391 391 if with_file_list:
392 392 return dict(
393 393 short_id=self.short_id,
394 394 raw_id=self.raw_id,
395 395 revision=self.revision,
396 396 message=self.message,
397 397 date=self.date,
398 398 author=self.author,
399 399 added=[el.path for el in self.added],
400 400 changed=[el.path for el in self.changed],
401 401 removed=[el.path for el in self.removed],
402 402 )
403 403 else:
404 404 return dict(
405 405 short_id=self.short_id,
406 406 raw_id=self.raw_id,
407 407 revision=self.revision,
408 408 message=self.message,
409 409 date=self.date,
410 410 author=self.author,
411 411 )
412 412
413 413 @LazyProperty
414 414 def last(self):
415 415 if self.repository is None:
416 416 raise ChangesetError("Cannot check if it's most recent revision")
417 417 return self.raw_id == self.repository.revisions[-1]
418 418
419 419 @LazyProperty
420 420 def parents(self):
421 421 """
422 422 Returns list of parents changesets.
423 423 """
424 424 raise NotImplementedError
425 425
426 426 @LazyProperty
427 427 def children(self):
428 428 """
429 429 Returns list of children changesets.
430 430 """
431 431 raise NotImplementedError
432 432
433 433 @LazyProperty
434 434 def raw_id(self):
435 435 """
436 436 Returns raw string identifying this changeset.
437 437 """
438 438 raise NotImplementedError
439 439
440 440 @LazyProperty
441 441 def short_id(self):
442 442 """
443 443 Returns shortened version of ``raw_id`` attribute, as string,
444 444 identifying this changeset, useful for web representation.
445 445 """
446 446 raise NotImplementedError
447 447
448 448 @LazyProperty
449 449 def revision(self):
450 450 """
451 451 Returns integer identifying this changeset.
452 452
453 453 """
454 454 raise NotImplementedError
455 455
456 456 @LazyProperty
457 457 def committer(self):
458 458 """
459 459 Returns Committer for given commit
460 460 """
461 461
462 462 raise NotImplementedError
463 463
464 464 @LazyProperty
465 465 def committer_name(self):
466 466 """
467 467 Returns Author name for given commit
468 468 """
469 469
470 470 return author_name(self.committer)
471 471
472 472 @LazyProperty
473 473 def committer_email(self):
474 474 """
475 475 Returns Author email address for given commit
476 476 """
477 477
478 478 return author_email(self.committer)
479 479
480 480 @LazyProperty
481 481 def author(self):
482 482 """
483 483 Returns Author for given commit
484 484 """
485 485
486 486 raise NotImplementedError
487 487
488 488 @LazyProperty
489 489 def author_name(self):
490 490 """
491 491 Returns Author name for given commit
492 492 """
493 493
494 494 return author_name(self.author)
495 495
496 496 @LazyProperty
497 497 def author_email(self):
498 498 """
499 499 Returns Author email address for given commit
500 500 """
501 501
502 502 return author_email(self.author)
503 503
504 504 def get_file_mode(self, path):
505 505 """
506 506 Returns stat mode of the file at the given ``path``.
507 507 """
508 508 raise NotImplementedError
509 509
510 510 def get_file_content(self, path):
511 511 """
512 512 Returns content of the file at the given ``path``.
513 513 """
514 514 raise NotImplementedError
515 515
516 516 def get_file_size(self, path):
517 517 """
518 518 Returns size of the file at the given ``path``.
519 519 """
520 520 raise NotImplementedError
521 521
522 522 def get_file_changeset(self, path):
523 523 """
524 524 Returns last commit of the file at the given ``path``.
525 525 """
526 526 raise NotImplementedError
527 527
528 528 def get_file_history(self, path):
529 529 """
530 530 Returns history of file as reversed list of ``Changeset`` objects for
531 531 which file at given ``path`` has been modified.
532 532 """
533 533 raise NotImplementedError
534 534
535 535 def get_nodes(self, path):
536 536 """
537 537 Returns combined ``DirNode`` and ``FileNode`` objects list representing
538 538 state of changeset at the given ``path``.
539 539
540 540 :raises ``ChangesetError``: if node at the given ``path`` is not
541 541 instance of ``DirNode``
542 542 """
543 543 raise NotImplementedError
544 544
545 545 def get_node(self, path):
546 546 """
547 547 Returns ``Node`` object from the given ``path``.
548 548
549 549 :raises ``NodeDoesNotExistError``: if there is no node at the given
550 550 ``path``
551 551 """
552 552 raise NotImplementedError
553 553
554 554 def fill_archive(self, stream=None, kind='tgz', prefix=None):
555 555 """
556 556 Fills up given stream.
557 557
558 558 :param stream: file like object.
559 559 :param kind: one of following: ``zip``, ``tar``, ``tgz``
560 560 or ``tbz2``. Default: ``tgz``.
561 561 :param prefix: name of root directory in archive.
562 562 Default is repository name and changeset's raw_id joined with dash.
563 563
564 564 repo-tip.<kind>
565 565 """
566 566
567 567 raise NotImplementedError
568 568
569 569 def get_chunked_archive(self, **kwargs):
570 570 """
571 571 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
572 572
573 573 :param chunk_size: extra parameter which controls size of returned
574 574 chunks. Default:8k.
575 575 """
576 576
577 577 chunk_size = kwargs.pop('chunk_size', 8192)
578 578 stream = kwargs.get('stream')
579 579 self.fill_archive(**kwargs)
580 580 while True:
581 581 data = stream.read(chunk_size)
582 582 if not data:
583 583 break
584 584 yield data
585 585
586 586 @LazyProperty
587 587 def root(self):
588 588 """
589 589 Returns ``RootNode`` object for this changeset.
590 590 """
591 591 return self.get_node('')
592 592
593 593 def next(self, branch=None):
594 594 """
595 595 Returns next changeset from current, if branch is gives it will return
596 596 next changeset belonging to this branch
597 597
598 598 :param branch: show changesets within the given named branch
599 599 """
600 600 raise NotImplementedError
601 601
602 602 def prev(self, branch=None):
603 603 """
604 604 Returns previous changeset from current, if branch is gives it will
605 605 return previous changeset belonging to this branch
606 606
607 607 :param branch: show changesets within the given named branch
608 608 """
609 609 raise NotImplementedError
610 610
611 611 @LazyProperty
612 612 def added(self):
613 613 """
614 614 Returns list of added ``FileNode`` objects.
615 615 """
616 616 raise NotImplementedError
617 617
618 618 @LazyProperty
619 619 def changed(self):
620 620 """
621 621 Returns list of modified ``FileNode`` objects.
622 622 """
623 623 raise NotImplementedError
624 624
625 625 @LazyProperty
626 626 def removed(self):
627 627 """
628 628 Returns list of removed ``FileNode`` objects.
629 629 """
630 630 raise NotImplementedError
631 631
632 632 @LazyProperty
633 633 def size(self):
634 634 """
635 635 Returns total number of bytes from contents of all filenodes.
636 636 """
637 637 return sum((node.size for node in self.get_filenodes_generator()))
638 638
639 639 def walk(self, topurl=''):
640 640 """
641 641 Similar to os.walk method. Instead of filesystem it walks through
642 642 changeset starting at given ``topurl``. Returns generator of tuples
643 643 (topnode, dirnodes, filenodes).
644 644 """
645 645 topnode = self.get_node(topurl)
646 646 yield (topnode, topnode.dirs, topnode.files)
647 647 for dirnode in topnode.dirs:
648 648 for tup in self.walk(dirnode.path):
649 649 yield tup
650 650
651 651 def get_filenodes_generator(self):
652 652 """
653 653 Returns generator that yields *all* file nodes.
654 654 """
655 655 for topnode, dirs, files in self.walk():
656 656 for node in files:
657 657 yield node
658 658
659 659 def as_dict(self):
660 660 """
661 661 Returns dictionary with changeset's attributes and their values.
662 662 """
663 663 data = get_dict_for_attrs(self, ['raw_id', 'short_id',
664 664 'revision', 'date', 'message'])
665 665 data['author'] = {'name': self.author_name, 'email': self.author_email}
666 666 data['added'] = [node.path for node in self.added]
667 667 data['changed'] = [node.path for node in self.changed]
668 668 data['removed'] = [node.path for node in self.removed]
669 669 return data
670 670
671 671 @LazyProperty
672 672 def closesbranch(self):
673 673 return False
674 674
675 675 @LazyProperty
676 676 def obsolete(self):
677 677 return False
678 678
679 679 @LazyProperty
680 680 def bumped(self):
681 681 return False
682 682
683 683 @LazyProperty
684 684 def divergent(self):
685 685 return False
686 686
687 687 @LazyProperty
688 688 def extinct(self):
689 689 return False
690 690
691 691 @LazyProperty
692 692 def unstable(self):
693 693 return False
694 694
695 695 @LazyProperty
696 696 def phase(self):
697 697 return ''
698 698
699 699
700 700 class BaseWorkdir(object):
701 701 """
702 702 Working directory representation of single repository.
703 703
704 704 :attribute: repository: repository object of working directory
705 705 """
706 706
707 707 def __init__(self, repository):
708 708 self.repository = repository
709 709
710 710 def get_branch(self):
711 711 """
712 712 Returns name of current branch.
713 713 """
714 714 raise NotImplementedError
715 715
716 716 def get_changeset(self):
717 717 """
718 718 Returns current changeset.
719 719 """
720 720 raise NotImplementedError
721 721
722 722 def get_added(self):
723 723 """
724 724 Returns list of ``FileNode`` objects marked as *new* in working
725 725 directory.
726 726 """
727 727 raise NotImplementedError
728 728
729 729 def get_changed(self):
730 730 """
731 731 Returns list of ``FileNode`` objects *changed* in working directory.
732 732 """
733 733 raise NotImplementedError
734 734
735 735 def get_removed(self):
736 736 """
737 737 Returns list of ``RemovedFileNode`` objects marked as *removed* in
738 738 working directory.
739 739 """
740 740 raise NotImplementedError
741 741
742 742 def get_untracked(self):
743 743 """
744 744 Returns list of ``FileNode`` objects which are present within working
745 745 directory however are not tracked by repository.
746 746 """
747 747 raise NotImplementedError
748 748
749 749 def get_status(self):
750 750 """
751 751 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
752 752 lists.
753 753 """
754 754 raise NotImplementedError
755 755
756 756 def commit(self, message, **kwargs):
757 757 """
758 758 Commits local (from working directory) changes and returns newly
759 759 created
760 760 ``Changeset``. Updates repository's ``revisions`` list.
761 761
762 762 :raises ``CommitError``: if any error occurs while committing
763 763 """
764 764 raise NotImplementedError
765 765
766 766 def update(self, revision=None):
767 767 """
768 768 Fetches content of the given revision and populates it within working
769 769 directory.
770 770 """
771 771 raise NotImplementedError
772 772
773 773 def checkout_branch(self, branch=None):
774 774 """
775 775 Checks out ``branch`` or the backend's default branch.
776 776
777 777 Raises ``BranchDoesNotExistError`` if the branch does not exist.
778 778 """
779 779 raise NotImplementedError
780 780
781 781
782 782 class BaseInMemoryChangeset(object):
783 783 """
784 784 Represents differences between repository's state (most recent head) and
785 785 changes made *in place*.
786 786
787 787 **Attributes**
788 788
789 789 ``repository``
790 790 repository object for this in-memory-changeset
791 791
792 792 ``added``
793 793 list of ``FileNode`` objects marked as *added*
794 794
795 795 ``changed``
796 796 list of ``FileNode`` objects marked as *changed*
797 797
798 798 ``removed``
799 799 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
800 800 *removed*
801 801
802 802 ``parents``
803 803 list of ``Changeset`` representing parents of in-memory changeset.
804 804 Should always be 2-element sequence.
805 805
806 806 """
807 807
808 808 def __init__(self, repository):
809 809 self.repository = repository
810 810 self.added = []
811 811 self.changed = []
812 812 self.removed = []
813 813 self.parents = []
814 814
815 815 def add(self, *filenodes):
816 816 """
817 817 Marks given ``FileNode`` objects as *to be committed*.
818 818
819 819 :raises ``NodeAlreadyExistsError``: if node with same path exists at
820 820 latest changeset
821 821 :raises ``NodeAlreadyAddedError``: if node with same path is already
822 822 marked as *added*
823 823 """
824 824 # Check if not already marked as *added* first
825 825 for node in filenodes:
826 826 if node.path in (n.path for n in self.added):
827 827 raise NodeAlreadyAddedError("Such FileNode %s is already "
828 828 "marked for addition" % node.path)
829 829 for node in filenodes:
830 830 self.added.append(node)
831 831
832 832 def change(self, *filenodes):
833 833 """
834 834 Marks given ``FileNode`` objects to be *changed* in next commit.
835 835
836 836 :raises ``EmptyRepositoryError``: if there are no changesets yet
837 837 :raises ``NodeAlreadyExistsError``: if node with same path is already
838 838 marked to be *changed*
839 839 :raises ``NodeAlreadyRemovedError``: if node with same path is already
840 840 marked to be *removed*
841 841 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
842 842 changeset
843 843 :raises ``NodeNotChangedError``: if node hasn't really be changed
844 844 """
845 845 for node in filenodes:
846 846 if node.path in (n.path for n in self.removed):
847 847 raise NodeAlreadyRemovedError("Node at %s is already marked "
848 848 "as removed" % node.path)
849 849 try:
850 850 self.repository.get_changeset()
851 851 except EmptyRepositoryError:
852 852 raise EmptyRepositoryError("Nothing to change - try to *add* new "
853 853 "nodes rather than changing them")
854 854 for node in filenodes:
855 855 if node.path in (n.path for n in self.changed):
856 856 raise NodeAlreadyChangedError("Node at '%s' is already "
857 857 "marked as changed" % node.path)
858 858 self.changed.append(node)
859 859
860 860 def remove(self, *filenodes):
861 861 """
862 862 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
863 863 *removed* in next commit.
864 864
865 865 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
866 866 be *removed*
867 867 :raises ``NodeAlreadyChangedError``: if node has been already marked to
868 868 be *changed*
869 869 """
870 870 for node in filenodes:
871 871 if node.path in (n.path for n in self.removed):
872 872 raise NodeAlreadyRemovedError("Node is already marked to "
873 873 "for removal at %s" % node.path)
874 874 if node.path in (n.path for n in self.changed):
875 875 raise NodeAlreadyChangedError("Node is already marked to "
876 876 "be changed at %s" % node.path)
877 877 # We only mark node as *removed* - real removal is done by
878 878 # commit method
879 879 self.removed.append(node)
880 880
881 881 def reset(self):
882 882 """
883 883 Resets this instance to initial state (cleans ``added``, ``changed``
884 884 and ``removed`` lists).
885 885 """
886 886 self.added = []
887 887 self.changed = []
888 888 self.removed = []
889 889 self.parents = []
890 890
891 891 def get_ipaths(self):
892 892 """
893 893 Returns generator of paths from nodes marked as added, changed or
894 894 removed.
895 895 """
896 896 for node in itertools.chain(self.added, self.changed, self.removed):
897 897 yield node.path
898 898
899 899 def get_paths(self):
900 900 """
901 901 Returns list of paths from nodes marked as added, changed or removed.
902 902 """
903 903 return list(self.get_ipaths())
904 904
905 905 def check_integrity(self, parents=None):
906 906 """
907 907 Checks in-memory changeset's integrity. Also, sets parents if not
908 908 already set.
909 909
910 910 :raises CommitError: if any error occurs (i.e.
911 911 ``NodeDoesNotExistError``).
912 912 """
913 913 if not self.parents:
914 914 parents = parents or []
915 915 if len(parents) == 0:
916 916 try:
917 917 parents = [self.repository.get_changeset(), None]
918 918 except EmptyRepositoryError:
919 919 parents = [None, None]
920 920 elif len(parents) == 1:
921 921 parents += [None]
922 922 self.parents = parents
923 923
924 924 # Local parents, only if not None
925 925 parents = [p for p in self.parents if p]
926 926
927 927 # Check nodes marked as added
928 928 for p in parents:
929 929 for node in self.added:
930 930 try:
931 931 p.get_node(node.path)
932 932 except NodeDoesNotExistError:
933 933 pass
934 934 else:
935 935 raise NodeAlreadyExistsError("Node at %s already exists "
936 936 "at %s" % (node.path, p))
937 937
938 938 # Check nodes marked as changed
939 939 missing = set(node.path for node in self.changed)
940 940 not_changed = set(node.path for node in self.changed)
941 941 if self.changed and not parents:
942 942 raise NodeDoesNotExistError(self.changed[0].path)
943 943 for p in parents:
944 944 for node in self.changed:
945 945 try:
946 946 old = p.get_node(node.path)
947 947 missing.remove(node.path)
948 948 # if content actually changed, remove node from unchanged
949 949 if old.content != node.content:
950 950 not_changed.remove(node.path)
951 951 except NodeDoesNotExistError:
952 952 pass
953 953 if self.changed and missing:
954 954 raise NodeDoesNotExistError("Node at %s is missing "
955 955 "(parents: %s)" % (node.path, parents))
956 956
957 957 if self.changed and not_changed:
958 958 raise NodeNotChangedError("Node at %s wasn't actually changed "
959 959 "since parents' changesets: %s" % (not_changed.pop(),
960 960 parents)
961 961 )
962 962
963 963 # Check nodes marked as removed
964 964 if self.removed and not parents:
965 965 raise NodeDoesNotExistError("Cannot remove node at %s as there "
966 966 "were no parents specified" % self.removed[0].path)
967 967 really_removed = set()
968 968 for p in parents:
969 969 for node in self.removed:
970 970 try:
971 971 p.get_node(node.path)
972 972 really_removed.add(node.path)
973 973 except ChangesetError:
974 974 pass
975 975 not_removed = list(set(node.path for node in self.removed) - really_removed)
976 976 if not_removed:
977 977 raise NodeDoesNotExistError("Cannot remove node at %s from "
978 978 "following parents: %s" % (not_removed[0], parents))
979 979
980 980 def commit(self, message, author, parents=None, branch=None, date=None,
981 981 **kwargs):
982 982 """
983 983 Performs in-memory commit (doesn't check workdir in any way) and
984 984 returns newly created ``Changeset``. Updates repository's
985 985 ``revisions``.
986 986
987 987 .. note::
988 988 While overriding this method each backend's should call
989 989 ``self.check_integrity(parents)`` in the first place.
990 990
991 991 :param message: message of the commit
992 992 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
993 993 :param parents: single parent or sequence of parents from which commit
994 994 would be derived
995 995 :param date: ``datetime.datetime`` instance. Defaults to
996 996 ``datetime.datetime.now()``.
997 997 :param branch: branch name, as string. If none given, default backend's
998 998 branch would be used.
999 999
1000 1000 :raises ``CommitError``: if any error occurs while committing
1001 1001 """
1002 1002 raise NotImplementedError
1003 1003
1004 1004
1005 1005 class EmptyChangeset(BaseChangeset):
1006 1006 """
1007 1007 An dummy empty changeset. It's possible to pass hash when creating
1008 1008 an EmptyChangeset
1009 1009 """
1010 1010
1011 1011 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1012 1012 alias=None, revision=-1, message='', author='', date=None):
1013 1013 self._empty_cs = cs
1014 1014 self.revision = revision
1015 1015 self.message = message
1016 1016 self.author = author
1017 1017 self.date = date or datetime.datetime.fromtimestamp(0)
1018 1018 self.repository = repo
1019 1019 self.requested_revision = requested_revision
1020 1020 self.alias = alias
1021 1021
1022 1022 @LazyProperty
1023 1023 def raw_id(self):
1024 1024 """
1025 1025 Returns raw string identifying this changeset, useful for web
1026 1026 representation.
1027 1027 """
1028 1028
1029 1029 return self._empty_cs
1030 1030
1031 1031 @LazyProperty
1032 1032 def branch(self):
1033 1033 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1034 1034
1035 1035 @LazyProperty
1036 1036 def branches(self):
1037 1037 return [self.branch]
1038 1038
1039 1039 @LazyProperty
1040 1040 def short_id(self):
1041 1041 return self.raw_id[:12]
1042 1042
1043 1043 def get_file_changeset(self, path):
1044 1044 return self
1045 1045
1046 1046 def get_file_content(self, path):
1047 1047 return b''
1048 1048
1049 1049 def get_file_size(self, path):
1050 1050 return 0
1051 1051
1052 1052
1053 1053 class CollectionGenerator(object):
1054 1054
1055 1055 def __init__(self, repo, revs):
1056 1056 self.repo = repo
1057 1057 self.revs = revs
1058 1058
1059 1059 def __len__(self):
1060 1060 return len(self.revs)
1061 1061
1062 1062 def __iter__(self):
1063 1063 for rev in self.revs:
1064 1064 yield self.repo.get_changeset(rev)
1065 1065
1066 1066 def __getitem__(self, what):
1067 1067 """Return either a single element by index, or a sliced collection."""
1068 1068 if isinstance(what, slice):
1069 1069 return CollectionGenerator(self.repo, self.revs[what])
1070 1070 else:
1071 1071 # single item
1072 1072 return self.repo.get_changeset(self.revs[what])
1073 1073
1074 1074 def __repr__(self):
1075 1075 return '<CollectionGenerator[len:%s]>' % (len(self))
@@ -1,690 +1,690 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.hg.repository
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Mercurial repository implementation.
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 logging
14 14 import os
15 15 import time
16 16 import urllib.error
17 17 import urllib.parse
18 18 import urllib.request
19 19 from collections import OrderedDict
20 20
21 21 import mercurial.commands
22 22 import mercurial.error
23 23 import mercurial.exchange
24 24 import mercurial.hg
25 25 import mercurial.hgweb
26 26 import mercurial.httppeer
27 27 import mercurial.localrepo
28 28 import mercurial.match
29 29 import mercurial.mdiff
30 30 import mercurial.node
31 31 import mercurial.patch
32 32 import mercurial.scmutil
33 33 import mercurial.sshpeer
34 34 import mercurial.tags
35 35 import mercurial.ui
36 36 import mercurial.unionrepo
37 37
38 38
39 39 try:
40 40 from mercurial.utils.urlutil import url as hg_url
41 41 except ImportError: # urlutil was introduced in Mercurial 5.8
42 42 from mercurial.util import url as hg_url
43 43
44 44 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
45 45 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
46 46 TagDoesNotExistError, VCSError)
47 47 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
48 48 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
49 49 from kallithea.lib.vcs.utils.lazy import LazyProperty
50 50 from kallithea.lib.vcs.utils.paths import abspath
51 51
52 52 from . import changeset, inmemory, workdir
53 53
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class MercurialRepository(BaseRepository):
59 59 """
60 60 Mercurial repository backend
61 61 """
62 62 DEFAULT_BRANCH_NAME = 'default'
63 63 scm = 'hg'
64 64
65 65 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
66 66 update_after_clone=False):
67 67 """
68 68 Raises RepositoryError if repository could not be find at the given
69 69 ``repo_path``.
70 70
71 71 :param repo_path: local path of the repository
72 72 :param create=False: if set to True, would try to create repository if
73 73 it does not exist rather than raising exception
74 74 :param baseui=None: user data
75 75 :param src_url=None: would try to clone repository from given location
76 76 :param update_after_clone=False: sets update of working copy after
77 77 making a clone
78 78 """
79 79
80 80 if not isinstance(repo_path, str):
81 81 raise VCSError('Mercurial backend requires repository path to '
82 82 'be instance of <str> got %s instead' %
83 83 type(repo_path))
84 84 self.path = abspath(repo_path)
85 85 self.baseui = baseui or mercurial.ui.ui()
86 86 # We've set path and ui, now we can set _repo itself
87 87 self._repo = self._get_repo(create, src_url, update_after_clone)
88 88
89 89 @property
90 90 def _empty(self):
91 91 """
92 92 Checks if repository is empty ie. without any changesets
93 93 """
94 94 # TODO: Following raises errors when using InMemoryChangeset...
95 95 # return len(self._repo.changelog) == 0
96 96 return len(self.revisions) == 0
97 97
98 98 @LazyProperty
99 99 def revisions(self):
100 100 """
101 101 Returns list of revisions' ids, in ascending order. Being lazy
102 102 attribute allows external tools to inject shas from cache.
103 103 """
104 104 return self._get_all_revisions()
105 105
106 106 @LazyProperty
107 107 def name(self):
108 108 return os.path.basename(self.path)
109 109
110 110 @LazyProperty
111 111 def branches(self):
112 112 return self._get_branches()
113 113
114 114 @LazyProperty
115 115 def closed_branches(self):
116 116 return self._get_branches(normal=False, closed=True)
117 117
118 118 @LazyProperty
119 119 def allbranches(self):
120 120 """
121 121 List all branches, including closed branches.
122 122 """
123 123 return self._get_branches(closed=True)
124 124
125 125 def _get_branches(self, normal=True, closed=False):
126 126 """
127 127 Gets branches for this repository
128 128 Returns only not closed branches by default
129 129
130 130 :param closed: return also closed branches for mercurial
131 131 :param normal: return also normal branches
132 132 """
133 133
134 134 if self._empty:
135 135 return {}
136 136
137 137 bt = OrderedDict()
138 138 for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
139 139 if isclosed:
140 140 if closed:
141 141 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
142 142 else:
143 143 if normal:
144 144 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
145 145 return bt
146 146
147 147 @LazyProperty
148 148 def tags(self):
149 149 """
150 150 Gets tags for this repository
151 151 """
152 152 return self._get_tags()
153 153
154 154 def _get_tags(self):
155 155 if self._empty:
156 156 return {}
157 157
158 158 return OrderedDict(sorted(
159 159 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
160 160 reverse=True,
161 161 key=lambda x: x[0], # sort by name
162 162 ))
163 163
164 164 def tag(self, name, user, revision=None, message=None, date=None,
165 165 **kwargs):
166 166 """
167 167 Creates and returns a tag for the given ``revision``.
168 168
169 169 :param name: name for new tag
170 170 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
171 171 :param revision: changeset id for which new tag would be created
172 172 :param message: message of the tag's commit
173 173 :param date: date of tag's commit
174 174
175 175 :raises TagAlreadyExistError: if tag with same name already exists
176 176 """
177 177 if name in self.tags:
178 178 raise TagAlreadyExistError("Tag %s already exists" % name)
179 179 changeset = self.get_changeset(revision)
180 180 local = kwargs.setdefault('local', False)
181 181
182 182 if message is None:
183 183 message = "Added tag %s for changeset %s" % (name,
184 184 changeset.short_id)
185 185
186 186 if date is None:
187 187 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
188 188
189 189 try:
190 190 mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
191 191 except mercurial.error.Abort as e:
192 192 raise RepositoryError(e.args[0])
193 193
194 194 # Reinitialize tags
195 195 self.tags = self._get_tags()
196 196 tag_id = self.tags[name]
197 197
198 198 return self.get_changeset(revision=tag_id)
199 199
200 200 def remove_tag(self, name, user, message=None, date=None):
201 201 """
202 202 Removes tag with the given ``name``.
203 203
204 204 :param name: name of the tag to be removed
205 205 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
206 206 :param message: message of the tag's removal commit
207 207 :param date: date of tag's removal commit
208 208
209 209 :raises TagDoesNotExistError: if tag with given name does not exists
210 210 """
211 211 if name not in self.tags:
212 212 raise TagDoesNotExistError("Tag %s does not exist" % name)
213 213 if message is None:
214 214 message = "Removed tag %s" % name
215 215 if date is None:
216 216 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
217 217 local = False
218 218
219 219 try:
220 220 mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.node.nullid, safe_bytes(message), local, safe_bytes(user), date)
221 221 self.tags = self._get_tags()
222 222 except mercurial.error.Abort as e:
223 223 raise RepositoryError(e.args[0])
224 224
225 225 @LazyProperty
226 226 def bookmarks(self):
227 227 """
228 228 Gets bookmarks for this repository
229 229 """
230 230 return self._get_bookmarks()
231 231
232 232 def _get_bookmarks(self):
233 233 if self._empty:
234 234 return {}
235 235
236 236 return OrderedDict(sorted(
237 237 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo._bookmarks.items()),
238 238 reverse=True,
239 239 key=lambda x: x[0], # sort by name
240 240 ))
241 241
242 242 def _get_all_revisions(self):
243 243 return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
244 244
245 245 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
246 246 context=3):
247 247 """
248 248 Returns (git like) *diff*, as plain text. Shows changes introduced by
249 249 ``rev2`` since ``rev1``.
250 250
251 251 :param rev1: Entry point from which diff is shown. Can be
252 252 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
253 253 the changes since empty state of the repository until ``rev2``
254 254 :param rev2: Until which revision changes should be shown.
255 255 :param ignore_whitespace: If set to ``True``, would not show whitespace
256 256 changes. Defaults to ``False``.
257 257 :param context: How many lines before/after changed lines should be
258 258 shown. Defaults to ``3``. If negative value is passed-in, it will be
259 259 set to ``0`` instead.
260 260 """
261 261
262 262 # Negative context values make no sense, and will result in
263 263 # errors. Ensure this does not happen.
264 264 if context < 0:
265 265 context = 0
266 266
267 267 if hasattr(rev1, 'raw_id'):
268 268 rev1 = getattr(rev1, 'raw_id')
269 269
270 270 if hasattr(rev2, 'raw_id'):
271 271 rev2 = getattr(rev2, 'raw_id')
272 272
273 273 # Check if given revisions are present at repository (may raise
274 274 # ChangesetDoesNotExistError)
275 275 if rev1 != self.EMPTY_CHANGESET:
276 276 self.get_changeset(rev1)
277 277 self.get_changeset(rev2)
278 278 if path:
279 279 file_filter = mercurial.match.exact([safe_bytes(path)])
280 280 else:
281 281 file_filter = None
282 282
283 283 return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
284 284 opts=mercurial.mdiff.diffopts(git=True,
285 285 showfunc=True,
286 286 ignorews=ignore_whitespace,
287 287 context=context)))
288 288
289 289 @staticmethod
290 290 def _check_url(url, repoui=None):
291 291 r"""
292 292 Raise URLError if url doesn't seem like a valid safe Hg URL. We
293 293 only allow http, https, ssh, and hg-git URLs.
294 294
295 295 For http, https and git URLs, make a connection and probe to see if it is valid.
296 296
297 297 On failures it'll raise urllib2.HTTPError, exception is also thrown
298 298 when the return code is non 200
299 299
300 300 >>> MercurialRepository._check_url('file:///repo')
301 301
302 302 >>> MercurialRepository._check_url('http://example.com:65537/repo')
303 303 Traceback (most recent call last):
304 304 ...
305 305 urllib.error.URLError: <urlopen error Error parsing URL: 'http://example.com:65537/repo'>
306 306 >>> MercurialRepository._check_url('foo')
307 307 Traceback (most recent call last):
308 308 ...
309 309 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'foo'>
310 310 >>> MercurialRepository._check_url('git+ssh://example.com/my%20fine repo')
311 311 Traceback (most recent call last):
312 312 ...
313 313 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'git+ssh://example.com/my%20fine repo'>
314 314 >>> MercurialRepository._check_url('svn+http://example.com/repo')
315 315 Traceback (most recent call last):
316 316 ...
317 317 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'svn+http://example.com/repo'>
318 318 """
319 319 try:
320 320 parsed_url = urllib.parse.urlparse(url)
321 321 parsed_url.port # trigger netloc parsing which might raise ValueError
322 322 except ValueError:
323 323 raise urllib.error.URLError("Error parsing URL: %r" % url)
324 324
325 325 # check first if it's not an local url
326 326 if os.path.isabs(url) and os.path.isdir(url) or parsed_url.scheme == 'file':
327 327 # When creating repos, _get_url will use file protocol for local paths
328 328 return
329 329
330 330 if parsed_url.scheme not in ['http', 'https', 'ssh', 'git+http', 'git+https']:
331 331 raise urllib.error.URLError("Unsupported protocol in URL %r" % url)
332 332
333 333 url = safe_bytes(url)
334 334
335 335 if parsed_url.scheme == 'ssh':
336 336 # in case of invalid uri or authentication issues, sshpeer will
337 337 # throw an exception.
338 338 mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
339 339 return
340 340
341 341 if '+' in parsed_url.scheme: # strip 'git+' for hg-git URLs
342 342 url = url.split(b'+', 1)[1]
343 343
344 344 url_obj = hg_url(url)
345 345 test_uri, handlers = get_urllib_request_handlers(url_obj)
346 346
347 347 url_obj.passwd = b'*****'
348 348 cleaned_uri = str(url_obj)
349 349
350 350 o = urllib.request.build_opener(*handlers)
351 351 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
352 352 ('Accept', 'application/mercurial-0.1')]
353 353
354 354 req = urllib.request.Request(
355 355 "%s?%s" % (
356 356 safe_str(test_uri),
357 357 urllib.parse.urlencode({
358 358 'cmd': 'between',
359 359 'pairs': "%s-%s" % ('0' * 40, '0' * 40),
360 360 })
361 361 ))
362 362
363 363 try:
364 364 resp = o.open(req)
365 365 if resp.code != 200:
366 366 raise Exception('Return Code is not 200')
367 367 except Exception as e:
368 368 # means it cannot be cloned
369 369 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
370 370
371 371 if parsed_url.scheme in ['http', 'https']: # skip git+http://... etc
372 372 # now check if it's a proper hg repo
373 373 try:
374 374 mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
375 375 except Exception as e:
376 376 raise urllib.error.URLError(
377 377 "url [%s] does not look like an hg repo org_exc: %s"
378 378 % (cleaned_uri, e))
379 379
380 380 def _get_repo(self, create, src_url=None, update_after_clone=False):
381 381 """
382 382 Function will check for mercurial repository in given path and return
383 383 a localrepo object. If there is no repository in that path it will
384 384 raise an exception unless ``create`` parameter is set to True - in
385 385 that case repository would be created and returned.
386 386 If ``src_url`` is given, would try to clone repository from the
387 387 location at given clone_point. Additionally it'll make update to
388 388 working copy accordingly to ``update_after_clone`` flag
389 389 """
390 390 try:
391 391 if src_url:
392 392 url = self._get_url(src_url)
393 393 opts = {}
394 394 if not update_after_clone:
395 395 opts.update({'noupdate': True})
396 396 MercurialRepository._check_url(url, self.baseui)
397 397 mercurial.commands.clone(self.baseui, safe_bytes(url), safe_bytes(self.path), **opts)
398 398
399 399 # Don't try to create if we've already cloned repo
400 400 create = False
401 401 return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
402 402 except (mercurial.error.Abort, mercurial.error.RepoError) as err:
403 403 if create:
404 404 msg = "Cannot create repository at %s. Original error was %s" \
405 405 % (self.name, err)
406 406 else:
407 407 msg = "Not valid repository at %s. Original error was %s" \
408 408 % (self.name, err)
409 409 raise RepositoryError(msg)
410 410
411 411 @LazyProperty
412 412 def in_memory_changeset(self):
413 413 return inmemory.MercurialInMemoryChangeset(self)
414 414
415 415 @LazyProperty
416 416 def description(self):
417 417 _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
418 418 return safe_str(_desc or b'unknown')
419 419
420 420 @LazyProperty
421 421 def last_change(self):
422 422 """
423 423 Returns last change made on this repository as datetime object
424 424 """
425 425 return date_fromtimestamp(self._get_mtime(), makedate()[1])
426 426
427 427 def _get_mtime(self):
428 428 try:
429 429 return time.mktime(self.get_changeset().date.timetuple())
430 430 except RepositoryError:
431 431 # fallback to filesystem
432 432 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
433 433 st_path = os.path.join(self.path, '.hg', "store")
434 434 if os.path.exists(cl_path):
435 435 return os.stat(cl_path).st_mtime
436 436 else:
437 437 return os.stat(st_path).st_mtime
438 438
439 439 def _get_revision(self, revision):
440 440 """
441 441 Given any revision identifier, returns a 40 char string with revision hash.
442 442
443 443 :param revision: str or int or None
444 444 """
445 445 if self._empty:
446 446 raise EmptyRepositoryError("There are no changesets yet")
447 447
448 448 if revision in [-1, None]:
449 449 revision = b'tip'
450 450 elif isinstance(revision, str):
451 451 revision = safe_bytes(revision)
452 452
453 453 try:
454 454 if isinstance(revision, int):
455 455 return ascii_str(self._repo[revision].hex())
456 456 return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
457 457 except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
458 458 msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
459 459 raise ChangesetDoesNotExistError(msg)
460 460 except (LookupError, ):
461 461 msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
462 462 raise ChangesetDoesNotExistError(msg)
463 463
464 464 def get_ref_revision(self, ref_type, ref_name):
465 465 """
466 466 Returns revision number for the given reference.
467 467 """
468 468 if ref_type == 'rev' and not ref_name.strip('0'):
469 469 return self.EMPTY_CHANGESET
470 470 # lookup up the exact node id
471 471 _revset_predicates = {
472 472 'branch': 'branch',
473 473 'book': 'bookmark',
474 474 'tag': 'tag',
475 475 'rev': 'id',
476 476 }
477 477 # avoid expensive branch(x) iteration over whole repo
478 478 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
479 479 try:
480 480 revs = self._repo.revs(rev_spec, ref_name, ref_name)
481 481 except LookupError:
482 482 msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
483 483 raise ChangesetDoesNotExistError(msg)
484 484 except mercurial.error.RepoLookupError:
485 485 msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
486 486 raise ChangesetDoesNotExistError(msg)
487 487 if revs:
488 488 revision = revs.last()
489 489 else:
490 490 # TODO: just report 'not found'?
491 491 revision = ref_name
492 492
493 493 return self._get_revision(revision)
494 494
495 495 def _get_archives(self, archive_name='tip'):
496 496 allowed = self.baseui.configlist(b"web", b"allow_archive",
497 497 untrusted=True)
498 498 for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
499 499 if name in allowed or self._repo.ui.configbool(b"web",
500 500 b"allow" + name,
501 501 untrusted=True):
502 502 yield {"type": safe_str(name), "extension": ext, "node": archive_name}
503 503
504 504 def _get_url(self, url):
505 505 """
506 506 Returns normalized url. If schema is not given, fall back to
507 507 filesystem (``file:///``) schema.
508 508 """
509 509 if url != 'default' and '://' not in url:
510 510 url = "file:" + urllib.request.pathname2url(url)
511 511 return url
512 512
513 513 def get_changeset(self, revision=None):
514 514 """
515 515 Returns ``MercurialChangeset`` object representing repository's
516 516 changeset at the given ``revision``.
517 517 """
518 518 return changeset.MercurialChangeset(repository=self, revision=self._get_revision(revision))
519 519
520 520 def get_changesets(self, start=None, end=None, start_date=None,
521 521 end_date=None, branch_name=None, reverse=False, max_revisions=None):
522 522 """
523 523 Returns iterator of ``MercurialChangeset`` objects from start to end
524 524 (both are inclusive)
525 525
526 526 :param start: None, str, int or mercurial lookup format
527 527 :param end: None, str, int or mercurial lookup format
528 528 :param start_date:
529 529 :param end_date:
530 530 :param branch_name:
531 :param reversed: return changesets in reversed order
531 :param reverse: return changesets in reversed order
532 532 """
533 533 start_raw_id = self._get_revision(start)
534 534 start_pos = None if start is None else self.revisions.index(start_raw_id)
535 535 end_raw_id = self._get_revision(end)
536 536 end_pos = None if end is None else self.revisions.index(end_raw_id)
537 537
538 538 if start_pos is not None and end_pos is not None and start_pos > end_pos:
539 539 raise RepositoryError("Start revision '%s' cannot be "
540 540 "after end revision '%s'" % (start, end))
541 541
542 542 if branch_name and branch_name not in self.allbranches:
543 543 msg = "Branch %r not found in %s" % (branch_name, self.name)
544 544 raise BranchDoesNotExistError(msg)
545 545 if end_pos is not None:
546 546 end_pos += 1
547 547 # filter branches
548 548 filter_ = []
549 549 if branch_name:
550 550 filter_.append(b'branch("%s")' % safe_bytes(branch_name))
551 551 if start_date:
552 552 filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
553 553 if end_date:
554 554 filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
555 555 if filter_ or max_revisions:
556 556 if filter_:
557 557 revspec = b' and '.join(filter_)
558 558 else:
559 559 revspec = b'all()'
560 560 if max_revisions:
561 561 revspec = b'limit(%s, %d)' % (revspec, max_revisions)
562 562 revisions = mercurial.scmutil.revrange(self._repo, [revspec])
563 563 else:
564 564 revisions = self.revisions
565 565
566 566 # this is very much a hack to turn this into a list; a better solution
567 567 # would be to get rid of this function entirely and use revsets
568 568 revs = list(revisions)[start_pos:end_pos]
569 569 if reverse:
570 570 revs.reverse()
571 571
572 572 return CollectionGenerator(self, revs)
573 573
574 574 def get_diff_changesets(self, org_rev, other_repo, other_rev):
575 575 """
576 576 Returns lists of changesets that can be merged from this repo @org_rev
577 577 to other_repo @other_rev
578 578 ... and the other way
579 579 ... and the ancestors that would be used for merge
580 580
581 581 :param org_rev: the revision we want our compare to be made
582 582 :param other_repo: repo object, most likely the fork of org_repo. It has
583 583 all changesets that we need to obtain
584 584 :param other_rev: revision we want out compare to be made on other_repo
585 585 """
586 586 ancestors = None
587 587 if org_rev == other_rev:
588 588 org_changesets = []
589 589 other_changesets = []
590 590
591 591 else:
592 592 # case two independent repos
593 593 if self != other_repo:
594 594 hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
595 595 safe_bytes(other_repo.path),
596 596 safe_bytes(self.path))
597 597 # all ancestors of other_rev will be in other_repo and
598 598 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
599 599
600 600 # no remote compare do it on the same repository
601 601 else:
602 602 hgrepo = other_repo._repo
603 603
604 604 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
605 605 hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
606 606 if ancestors:
607 607 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
608 608 else:
609 609 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
610 610 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
611 611 hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
612 612
613 613 other_changesets = [
614 614 other_repo.get_changeset(rev)
615 615 for rev in hgrepo.revs(
616 616 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
617 617 ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
618 618 ]
619 619 org_changesets = [
620 620 self.get_changeset(ascii_str(hgrepo[rev].hex()))
621 621 for rev in hgrepo.revs(
622 622 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
623 623 ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
624 624 ]
625 625
626 626 return other_changesets, org_changesets, ancestors
627 627
628 628 def pull(self, url):
629 629 """
630 630 Tries to pull changes from external location.
631 631 """
632 632 other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
633 633 try:
634 634 mercurial.exchange.pull(self._repo, other, heads=None, force=None)
635 635 except mercurial.error.Abort as err:
636 636 # Propagate error but with vcs's type
637 637 raise RepositoryError(str(err))
638 638
639 639 @LazyProperty
640 640 def workdir(self):
641 641 """
642 642 Returns ``Workdir`` instance for this repository.
643 643 """
644 644 return workdir.MercurialWorkdir(self)
645 645
646 646 def get_config_value(self, section, name=None, config_file=None):
647 647 """
648 648 Returns configuration value for a given [``section``] and ``name``.
649 649
650 650 :param section: Section we want to retrieve value from
651 651 :param name: Name of configuration we want to retrieve
652 652 :param config_file: A path to file which should be used to retrieve
653 653 configuration from (might also be a list of file paths)
654 654 """
655 655 if config_file is None:
656 656 config_file = []
657 657 elif isinstance(config_file, str):
658 658 config_file = [config_file]
659 659
660 660 config = self._repo.ui
661 661 if config_file:
662 662 config = mercurial.ui.ui()
663 663 for path in config_file:
664 664 config.readconfig(safe_bytes(path))
665 665 value = config.config(safe_bytes(section), safe_bytes(name))
666 666 return value if value is None else safe_str(value)
667 667
668 668 def get_user_name(self, config_file=None):
669 669 """
670 670 Returns user's name from global configuration file.
671 671
672 672 :param config_file: A path to file which should be used to retrieve
673 673 configuration from (might also be a list of file paths)
674 674 """
675 675 username = self.get_config_value('ui', 'username', config_file=config_file)
676 676 if username:
677 677 return author_name(username)
678 678 return None
679 679
680 680 def get_user_email(self, config_file=None):
681 681 """
682 682 Returns user's email from global configuration file.
683 683
684 684 :param config_file: A path to file which should be used to retrieve
685 685 configuration from (might also be a list of file paths)
686 686 """
687 687 username = self.get_config_value('ui', 'username', config_file=config_file)
688 688 if username:
689 689 return author_email(username)
690 690 return None
General Comments 0
You need to be logged in to leave comments. Login now