##// END OF EJS Templates
Added VCS into rhodecode core for faster and easier deployments of new versions
marcink -
r2007:324ac367 beta
parent child Browse files
Show More
@@ -0,0 +1,41 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs
4 ~~~
5
6 Various version Control System (vcs) management abstraction layer for
7 Python.
8
9 :created_on: Apr 8, 2010
10 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
11 """
12
13 VERSION = (0, 2, 3, 'dev')
14
15 __version__ = '.'.join((str(each) for each in VERSION[:4]))
16
17 __all__ = [
18 'get_version', 'get_repo', 'get_backend',
19 'VCSError', 'RepositoryError', 'ChangesetError']
20
21 import sys
22 from rhodecode.lib.vcs.backends import get_repo, get_backend
23 from rhodecode.lib.vcs.exceptions import VCSError, RepositoryError, ChangesetError
24
25
26 def get_version():
27 """
28 Returns shorter version (digit parts only) as string.
29 """
30 return '.'.join((str(each) for each in VERSION[:3]))
31
32 def main(argv=None):
33 if argv is None:
34 argv = sys.argv
35 from rhodecode.lib.vcs.cli import ExecutionManager
36 manager = ExecutionManager(argv)
37 manager.execute()
38 return 0
39
40 if __name__ == '__main__':
41 sys.exit(main(sys.argv))
@@ -0,0 +1,63 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs.backends
4 ~~~~~~~~~~~~
5
6 Main package for scm backends
7
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
11 import os
12 from pprint import pformat
13 from rhodecode.lib.vcs.conf import settings
14 from rhodecode.lib.vcs.exceptions import VCSError
15 from rhodecode.lib.vcs.utils.helpers import get_scm
16 from rhodecode.lib.vcs.utils.paths import abspath
17 from rhodecode.lib.vcs.utils.imports import import_class
18
19
20 def get_repo(path=None, alias=None, create=False):
21 """
22 Returns ``Repository`` object of type linked with given ``alias`` at
23 the specified ``path``. If ``alias`` is not given it will try to guess it
24 using get_scm method
25 """
26 if create:
27 if not (path or alias):
28 raise TypeError("If create is specified, we need path and scm type")
29 return get_backend(alias)(path, create=True)
30 if path is None:
31 path = abspath(os.path.curdir)
32 try:
33 scm, path = get_scm(path, search_recursively=True)
34 path = abspath(path)
35 alias = scm
36 except VCSError:
37 raise VCSError("No scm found at %s" % path)
38 if alias is None:
39 alias = get_scm(path)[0]
40
41 backend = get_backend(alias)
42 repo = backend(path, create=create)
43 return repo
44
45
46 def get_backend(alias):
47 """
48 Returns ``Repository`` class identified by the given alias or raises
49 VCSError if alias is not recognized or backend class cannot be imported.
50 """
51 if alias not in settings.BACKENDS:
52 raise VCSError("Given alias '%s' is not recognized! Allowed aliases:\n"
53 "%s" % (alias, pformat(settings.BACKENDS.keys())))
54 backend_path = settings.BACKENDS[alias]
55 klass = import_class(backend_path)
56 return klass
57
58
59 def get_supported_backends():
60 """
61 Returns list of aliases of supported backends.
62 """
63 return settings.BACKENDS.keys()
This diff has been collapsed as it changes many lines, (911 lines changed) Show them Hide them
@@ -0,0 +1,911 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
5
6 Base for all available scm backends
7
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
11
12
13 from itertools import chain
14 from rhodecode.lib.vcs.utils import author_name, author_email
15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 from rhodecode.lib.vcs.conf import settings
18
19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 RepositoryError
23
24
25 class BaseRepository(object):
26 """
27 Base Repository for final backends
28
29 **Attributes**
30
31 ``DEFAULT_BRANCH_NAME``
32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33
34 ``scm``
35 alias of scm, i.e. *git* or *hg*
36
37 ``repo``
38 object from external api
39
40 ``revisions``
41 list of all available revisions' ids, in ascending order
42
43 ``changesets``
44 storage dict caching returned changesets
45
46 ``path``
47 absolute path to the repository
48
49 ``branches``
50 branches as list of changesets
51
52 ``tags``
53 tags as list of changesets
54 """
55 scm = None
56 DEFAULT_BRANCH_NAME = None
57 EMPTY_CHANGESET = '0' * 40
58
59 def __init__(self, repo_path, create=False, **kwargs):
60 """
61 Initializes repository. Raises RepositoryError if repository could
62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 exists and ``create`` is set to True.
64
65 :param repo_path: local path of the repository
66 :param create=False: if set to True, would try to craete repository.
67 :param src_url=None: if set, should be proper url from which repository
68 would be cloned; requires ``create`` parameter to be set to True -
69 raises RepositoryError if src_url is set and create evaluates to
70 False
71 """
72 raise NotImplementedError
73
74 def __str__(self):
75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76
77 def __repr__(self):
78 return self.__str__()
79
80 def __len__(self):
81 return self.count()
82
83 @LazyProperty
84 def alias(self):
85 for k, v in settings.BACKENDS.items():
86 if v.split('.')[-1] == str(self.__class__.__name__):
87 return k
88
89 @LazyProperty
90 def name(self):
91 raise NotImplementedError
92
93 @LazyProperty
94 def owner(self):
95 raise NotImplementedError
96
97 @LazyProperty
98 def description(self):
99 raise NotImplementedError
100
101 @LazyProperty
102 def size(self):
103 """
104 Returns combined size in bytes for all repository files
105 """
106
107 size = 0
108 try:
109 tip = self.get_changeset()
110 for topnode, dirs, files in tip.walk('/'):
111 for f in files:
112 size += tip.get_file_size(f.path)
113 for dir in dirs:
114 for f in files:
115 size += tip.get_file_size(f.path)
116
117 except RepositoryError, e:
118 pass
119 return size
120
121 def is_valid(self):
122 """
123 Validates repository.
124 """
125 raise NotImplementedError
126
127 def get_last_change(self):
128 self.get_changesets()
129
130 #==========================================================================
131 # CHANGESETS
132 #==========================================================================
133
134 def get_changeset(self, revision=None):
135 """
136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 recent changeset is returned.
138
139 :raises ``EmptyRepositoryError``: if there are no revisions
140 """
141 raise NotImplementedError
142
143 def __iter__(self):
144 """
145 Allows Repository objects to be iterated.
146
147 *Requires* implementation of ``__getitem__`` method.
148 """
149 for revision in self.revisions:
150 yield self.get_changeset(revision)
151
152 def get_changesets(self, start=None, end=None, start_date=None,
153 end_date=None, branch_name=None, reverse=False):
154 """
155 Returns iterator of ``MercurialChangeset`` objects from start to end
156 not inclusive This should behave just like a list, ie. end is not
157 inclusive
158
159 :param start: None or str
160 :param end: None or str
161 :param start_date:
162 :param end_date:
163 :param branch_name:
164 :param reversed:
165 """
166 raise NotImplementedError
167
168 def __getslice__(self, i, j):
169 """
170 Returns a iterator of sliced repository
171 """
172 for rev in self.revisions[i:j]:
173 yield self.get_changeset(rev)
174
175 def __getitem__(self, key):
176 return self.get_changeset(key)
177
178 def count(self):
179 return len(self.revisions)
180
181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 """
183 Creates and returns a tag for the given ``revision``.
184
185 :param name: name for new tag
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 :param revision: changeset id for which new tag would be created
188 :param message: message of the tag's commit
189 :param date: date of tag's commit
190
191 :raises TagAlreadyExistError: if tag with same name already exists
192 """
193 raise NotImplementedError
194
195 def remove_tag(self, name, user, message=None, date=None):
196 """
197 Removes tag with the given ``name``.
198
199 :param name: name of the tag to be removed
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 :param message: message of the tag's removal commit
202 :param date: date of tag's removal commit
203
204 :raises TagDoesNotExistError: if tag with given name does not exists
205 """
206 raise NotImplementedError
207
208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 context=3):
210 """
211 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 ``rev2`` since ``rev1``.
213
214 :param rev1: Entry point from which diff is shown. Can be
215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 the changes since empty state of the repository until ``rev2``
217 :param rev2: Until which revision changes should be shown.
218 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 changes. Defaults to ``False``.
220 :param context: How many lines before/after changed lines should be
221 shown. Defaults to ``3``.
222 """
223 raise NotImplementedError
224
225 # ========== #
226 # COMMIT API #
227 # ========== #
228
229 @LazyProperty
230 def in_memory_changeset(self):
231 """
232 Returns ``InMemoryChangeset`` object for this repository.
233 """
234 raise NotImplementedError
235
236 def add(self, filenode, **kwargs):
237 """
238 Commit api function that will add given ``FileNode`` into this
239 repository.
240
241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 already in repository
243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 *added*
245 """
246 raise NotImplementedError
247
248 def remove(self, filenode, **kwargs):
249 """
250 Commit api function that will remove given ``FileNode`` into this
251 repository.
252
253 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 """
256 raise NotImplementedError
257
258 def commit(self, message, **kwargs):
259 """
260 Persists current changes made on this repository and returns newly
261 created changeset.
262
263 :raises ``NothingChangedError``: if no changes has been made
264 """
265 raise NotImplementedError
266
267 def get_state(self):
268 """
269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 containing ``FileNode`` objects.
271 """
272 raise NotImplementedError
273
274 def get_config_value(self, section, name, config_file=None):
275 """
276 Returns configuration value for a given [``section``] and ``name``.
277
278 :param section: Section we want to retrieve value from
279 :param name: Name of configuration we want to retrieve
280 :param config_file: A path to file which should be used to retrieve
281 configuration from (might also be a list of file paths)
282 """
283 raise NotImplementedError
284
285 def get_user_name(self, config_file=None):
286 """
287 Returns user's name from global configuration file.
288
289 :param config_file: A path to file which should be used to retrieve
290 configuration from (might also be a list of file paths)
291 """
292 raise NotImplementedError
293
294 def get_user_email(self, config_file=None):
295 """
296 Returns user's email from global configuration file.
297
298 :param config_file: A path to file which should be used to retrieve
299 configuration from (might also be a list of file paths)
300 """
301 raise NotImplementedError
302
303 # =========== #
304 # WORKDIR API #
305 # =========== #
306
307 @LazyProperty
308 def workdir(self):
309 """
310 Returns ``Workdir`` instance for this repository.
311 """
312 raise NotImplementedError
313
314
315 class BaseChangeset(object):
316 """
317 Each backend should implement it's changeset representation.
318
319 **Attributes**
320
321 ``repository``
322 repository object within which changeset exists
323
324 ``id``
325 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
326
327 ``raw_id``
328 raw changeset representation (i.e. full 40 length sha for git
329 backend)
330
331 ``short_id``
332 shortened (if apply) version of ``raw_id``; it would be simple
333 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
334 as ``raw_id`` for subversion
335
336 ``revision``
337 revision number as integer
338
339 ``files``
340 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
341
342 ``dirs``
343 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
344
345 ``nodes``
346 combined list of ``Node`` objects
347
348 ``author``
349 author of the changeset, as unicode
350
351 ``message``
352 message of the changeset, as unicode
353
354 ``parents``
355 list of parent changesets
356
357 ``last``
358 ``True`` if this is last changeset in repository, ``False``
359 otherwise; trying to access this attribute while there is no
360 changesets would raise ``EmptyRepositoryError``
361 """
362 def __str__(self):
363 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
364 self.short_id)
365
366 def __repr__(self):
367 return self.__str__()
368
369 def __unicode__(self):
370 return u'%s:%s' % (self.revision, self.short_id)
371
372 def __eq__(self, other):
373 return self.raw_id == other.raw_id
374
375 @LazyProperty
376 def last(self):
377 if self.repository is None:
378 raise ChangesetError("Cannot check if it's most recent revision")
379 return self.raw_id == self.repository.revisions[-1]
380
381 @LazyProperty
382 def parents(self):
383 """
384 Returns list of parents changesets.
385 """
386 raise NotImplementedError
387
388 @LazyProperty
389 def id(self):
390 """
391 Returns string identifying this changeset.
392 """
393 raise NotImplementedError
394
395 @LazyProperty
396 def raw_id(self):
397 """
398 Returns raw string identifying this changeset.
399 """
400 raise NotImplementedError
401
402 @LazyProperty
403 def short_id(self):
404 """
405 Returns shortened version of ``raw_id`` attribute, as string,
406 identifying this changeset, useful for web representation.
407 """
408 raise NotImplementedError
409
410 @LazyProperty
411 def revision(self):
412 """
413 Returns integer identifying this changeset.
414
415 """
416 raise NotImplementedError
417
418 @LazyProperty
419 def author(self):
420 """
421 Returns Author for given commit
422 """
423
424 raise NotImplementedError
425
426 @LazyProperty
427 def author_name(self):
428 """
429 Returns Author name for given commit
430 """
431
432 return author_name(self.author)
433
434 @LazyProperty
435 def author_email(self):
436 """
437 Returns Author email address for given commit
438 """
439
440 return author_email(self.author)
441
442 def get_file_mode(self, path):
443 """
444 Returns stat mode of the file at the given ``path``.
445 """
446 raise NotImplementedError
447
448 def get_file_content(self, path):
449 """
450 Returns content of the file at the given ``path``.
451 """
452 raise NotImplementedError
453
454 def get_file_size(self, path):
455 """
456 Returns size of the file at the given ``path``.
457 """
458 raise NotImplementedError
459
460 def get_file_changeset(self, path):
461 """
462 Returns last commit of the file at the given ``path``.
463 """
464 raise NotImplementedError
465
466 def get_file_history(self, path):
467 """
468 Returns history of file as reversed list of ``Changeset`` objects for
469 which file at given ``path`` has been modified.
470 """
471 raise NotImplementedError
472
473 def get_nodes(self, path):
474 """
475 Returns combined ``DirNode`` and ``FileNode`` objects list representing
476 state of changeset at the given ``path``.
477
478 :raises ``ChangesetError``: if node at the given ``path`` is not
479 instance of ``DirNode``
480 """
481 raise NotImplementedError
482
483 def get_node(self, path):
484 """
485 Returns ``Node`` object from the given ``path``.
486
487 :raises ``NodeDoesNotExistError``: if there is no node at the given
488 ``path``
489 """
490 raise NotImplementedError
491
492 def fill_archive(self, stream=None, kind='tgz', prefix=None):
493 """
494 Fills up given stream.
495
496 :param stream: file like object.
497 :param kind: one of following: ``zip``, ``tar``, ``tgz``
498 or ``tbz2``. Default: ``tgz``.
499 :param prefix: name of root directory in archive.
500 Default is repository name and changeset's raw_id joined with dash.
501
502 repo-tip.<kind>
503 """
504
505 raise NotImplementedError
506
507 def get_chunked_archive(self, **kwargs):
508 """
509 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
510
511 :param chunk_size: extra parameter which controls size of returned
512 chunks. Default:8k.
513 """
514
515 chunk_size = kwargs.pop('chunk_size', 8192)
516 stream = kwargs.get('stream')
517 self.fill_archive(**kwargs)
518 while True:
519 data = stream.read(chunk_size)
520 if not data:
521 break
522 yield data
523
524 @LazyProperty
525 def root(self):
526 """
527 Returns ``RootNode`` object for this changeset.
528 """
529 return self.get_node('')
530
531 def next(self, branch=None):
532 """
533 Returns next changeset from current, if branch is gives it will return
534 next changeset belonging to this branch
535
536 :param branch: show changesets within the given named branch
537 """
538 raise NotImplementedError
539
540 def prev(self, branch=None):
541 """
542 Returns previous changeset from current, if branch is gives it will
543 return previous changeset belonging to this branch
544
545 :param branch: show changesets within the given named branch
546 """
547 raise NotImplementedError
548
549 @LazyProperty
550 def added(self):
551 """
552 Returns list of added ``FileNode`` objects.
553 """
554 raise NotImplementedError
555
556 @LazyProperty
557 def changed(self):
558 """
559 Returns list of modified ``FileNode`` objects.
560 """
561 raise NotImplementedError
562
563 @LazyProperty
564 def removed(self):
565 """
566 Returns list of removed ``FileNode`` objects.
567 """
568 raise NotImplementedError
569
570 @LazyProperty
571 def size(self):
572 """
573 Returns total number of bytes from contents of all filenodes.
574 """
575 return sum((node.size for node in self.get_filenodes_generator()))
576
577 def walk(self, topurl=''):
578 """
579 Similar to os.walk method. Insted of filesystem it walks through
580 changeset starting at given ``topurl``. Returns generator of tuples
581 (topnode, dirnodes, filenodes).
582 """
583 topnode = self.get_node(topurl)
584 yield (topnode, topnode.dirs, topnode.files)
585 for dirnode in topnode.dirs:
586 for tup in self.walk(dirnode.path):
587 yield tup
588
589 def get_filenodes_generator(self):
590 """
591 Returns generator that yields *all* file nodes.
592 """
593 for topnode, dirs, files in self.walk():
594 for node in files:
595 yield node
596
597 def as_dict(self):
598 """
599 Returns dictionary with changeset's attributes and their values.
600 """
601 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
602 'revision', 'date', 'message'])
603 data['author'] = {'name': self.author_name, 'email': self.author_email}
604 data['added'] = [node.path for node in self.added]
605 data['changed'] = [node.path for node in self.changed]
606 data['removed'] = [node.path for node in self.removed]
607 return data
608
609
610 class BaseWorkdir(object):
611 """
612 Working directory representation of single repository.
613
614 :attribute: repository: repository object of working directory
615 """
616
617 def __init__(self, repository):
618 self.repository = repository
619
620 def get_branch(self):
621 """
622 Returns name of current branch.
623 """
624 raise NotImplementedError
625
626 def get_changeset(self):
627 """
628 Returns current changeset.
629 """
630 raise NotImplementedError
631
632 def get_added(self):
633 """
634 Returns list of ``FileNode`` objects marked as *new* in working
635 directory.
636 """
637 raise NotImplementedError
638
639 def get_changed(self):
640 """
641 Returns list of ``FileNode`` objects *changed* in working directory.
642 """
643 raise NotImplementedError
644
645 def get_removed(self):
646 """
647 Returns list of ``RemovedFileNode`` objects marked as *removed* in
648 working directory.
649 """
650 raise NotImplementedError
651
652 def get_untracked(self):
653 """
654 Returns list of ``FileNode`` objects which are present within working
655 directory however are not tracked by repository.
656 """
657 raise NotImplementedError
658
659 def get_status(self):
660 """
661 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
662 lists.
663 """
664 raise NotImplementedError
665
666 def commit(self, message, **kwargs):
667 """
668 Commits local (from working directory) changes and returns newly
669 created
670 ``Changeset``. Updates repository's ``revisions`` list.
671
672 :raises ``CommitError``: if any error occurs while committing
673 """
674 raise NotImplementedError
675
676 def update(self, revision=None):
677 """
678 Fetches content of the given revision and populates it within working
679 directory.
680 """
681 raise NotImplementedError
682
683 def checkout_branch(self, branch=None):
684 """
685 Checks out ``branch`` or the backend's default branch.
686
687 Raises ``BranchDoesNotExistError`` if the branch does not exist.
688 """
689 raise NotImplementedError
690
691
692 class BaseInMemoryChangeset(object):
693 """
694 Represents differences between repository's state (most recent head) and
695 changes made *in place*.
696
697 **Attributes**
698
699 ``repository``
700 repository object for this in-memory-changeset
701
702 ``added``
703 list of ``FileNode`` objects marked as *added*
704
705 ``changed``
706 list of ``FileNode`` objects marked as *changed*
707
708 ``removed``
709 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
710 *removed*
711
712 ``parents``
713 list of ``Changeset`` representing parents of in-memory changeset.
714 Should always be 2-element sequence.
715
716 """
717
718 def __init__(self, repository):
719 self.repository = repository
720 self.added = []
721 self.changed = []
722 self.removed = []
723 self.parents = []
724
725 def add(self, *filenodes):
726 """
727 Marks given ``FileNode`` objects as *to be committed*.
728
729 :raises ``NodeAlreadyExistsError``: if node with same path exists at
730 latest changeset
731 :raises ``NodeAlreadyAddedError``: if node with same path is already
732 marked as *added*
733 """
734 # Check if not already marked as *added* first
735 for node in filenodes:
736 if node.path in (n.path for n in self.added):
737 raise NodeAlreadyAddedError("Such FileNode %s is already "
738 "marked for addition" % node.path)
739 for node in filenodes:
740 self.added.append(node)
741
742 def change(self, *filenodes):
743 """
744 Marks given ``FileNode`` objects to be *changed* in next commit.
745
746 :raises ``EmptyRepositoryError``: if there are no changesets yet
747 :raises ``NodeAlreadyExistsError``: if node with same path is already
748 marked to be *changed*
749 :raises ``NodeAlreadyRemovedError``: if node with same path is already
750 marked to be *removed*
751 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
752 changeset
753 :raises ``NodeNotChangedError``: if node hasn't really be changed
754 """
755 for node in filenodes:
756 if node.path in (n.path for n in self.removed):
757 raise NodeAlreadyRemovedError("Node at %s is already marked "
758 "as removed" % node.path)
759 try:
760 self.repository.get_changeset()
761 except EmptyRepositoryError:
762 raise EmptyRepositoryError("Nothing to change - try to *add* new "
763 "nodes rather than changing them")
764 for node in filenodes:
765 if node.path in (n.path for n in self.changed):
766 raise NodeAlreadyChangedError("Node at '%s' is already "
767 "marked as changed" % node.path)
768 self.changed.append(node)
769
770 def remove(self, *filenodes):
771 """
772 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
773 *removed* in next commit.
774
775 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
776 be *removed*
777 :raises ``NodeAlreadyChangedError``: if node has been already marked to
778 be *changed*
779 """
780 for node in filenodes:
781 if node.path in (n.path for n in self.removed):
782 raise NodeAlreadyRemovedError("Node is already marked to "
783 "for removal at %s" % node.path)
784 if node.path in (n.path for n in self.changed):
785 raise NodeAlreadyChangedError("Node is already marked to "
786 "be changed at %s" % node.path)
787 # We only mark node as *removed* - real removal is done by
788 # commit method
789 self.removed.append(node)
790
791 def reset(self):
792 """
793 Resets this instance to initial state (cleans ``added``, ``changed``
794 and ``removed`` lists).
795 """
796 self.added = []
797 self.changed = []
798 self.removed = []
799 self.parents = []
800
801 def get_ipaths(self):
802 """
803 Returns generator of paths from nodes marked as added, changed or
804 removed.
805 """
806 for node in chain(self.added, self.changed, self.removed):
807 yield node.path
808
809 def get_paths(self):
810 """
811 Returns list of paths from nodes marked as added, changed or removed.
812 """
813 return list(self.get_ipaths())
814
815 def check_integrity(self, parents=None):
816 """
817 Checks in-memory changeset's integrity. Also, sets parents if not
818 already set.
819
820 :raises CommitError: if any error occurs (i.e.
821 ``NodeDoesNotExistError``).
822 """
823 if not self.parents:
824 parents = parents or []
825 if len(parents) == 0:
826 try:
827 parents = [self.repository.get_changeset(), None]
828 except EmptyRepositoryError:
829 parents = [None, None]
830 elif len(parents) == 1:
831 parents += [None]
832 self.parents = parents
833
834 # Local parents, only if not None
835 parents = [p for p in self.parents if p]
836
837 # Check nodes marked as added
838 for p in parents:
839 for node in self.added:
840 try:
841 p.get_node(node.path)
842 except NodeDoesNotExistError:
843 pass
844 else:
845 raise NodeAlreadyExistsError("Node at %s already exists "
846 "at %s" % (node.path, p))
847
848 # Check nodes marked as changed
849 missing = set(self.changed)
850 not_changed = set(self.changed)
851 if self.changed and not parents:
852 raise NodeDoesNotExistError(str(self.changed[0].path))
853 for p in parents:
854 for node in self.changed:
855 try:
856 old = p.get_node(node.path)
857 missing.remove(node)
858 if old.content != node.content:
859 not_changed.remove(node)
860 except NodeDoesNotExistError:
861 pass
862 if self.changed and missing:
863 raise NodeDoesNotExistError("Node at %s is missing "
864 "(parents: %s)" % (node.path, parents))
865
866 if self.changed and not_changed:
867 raise NodeNotChangedError("Node at %s wasn't actually changed "
868 "since parents' changesets: %s" % (not_changed.pop().path,
869 parents)
870 )
871
872 # Check nodes marked as removed
873 if self.removed and not parents:
874 raise NodeDoesNotExistError("Cannot remove node at %s as there "
875 "were no parents specified" % self.removed[0].path)
876 really_removed = set()
877 for p in parents:
878 for node in self.removed:
879 try:
880 p.get_node(node.path)
881 really_removed.add(node)
882 except ChangesetError:
883 pass
884 not_removed = set(self.removed) - really_removed
885 if not_removed:
886 raise NodeDoesNotExistError("Cannot remove node at %s from "
887 "following parents: %s" % (not_removed[0], parents))
888
889 def commit(self, message, author, parents=None, branch=None, date=None,
890 **kwargs):
891 """
892 Performs in-memory commit (doesn't check workdir in any way) and
893 returns newly created ``Changeset``. Updates repository's
894 ``revisions``.
895
896 .. note::
897 While overriding this method each backend's should call
898 ``self.check_integrity(parents)`` in the first place.
899
900 :param message: message of the commit
901 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
902 :param parents: single parent or sequence of parents from which commit
903 would be derieved
904 :param date: ``datetime.datetime`` instance. Defaults to
905 ``datetime.datetime.now()``.
906 :param branch: branch name, as string. If none given, default backend's
907 branch would be used.
908
909 :raises ``CommitError``: if any error occurs while committing
910 """
911 raise NotImplementedError
@@ -0,0 +1,9 b''
1 from .repository import GitRepository
2 from .changeset import GitChangeset
3 from .inmemory import GitInMemoryChangeset
4 from .workdir import GitWorkdir
5
6
7 __all__ = [
8 'GitRepository', 'GitChangeset', 'GitInMemoryChangeset', 'GitWorkdir',
9 ]
@@ -0,0 +1,450 b''
1 import re
2 from itertools import chain
3 from dulwich import objects
4 from subprocess import Popen, PIPE
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 from rhodecode.lib.vcs.exceptions import VCSError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode
14 from rhodecode.lib.vcs.utils import safe_unicode
15 from rhodecode.lib.vcs.utils import date_fromtimestamp
16 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17
18
19 class GitChangeset(BaseChangeset):
20 """
21 Represents state of the repository at single revision.
22 """
23
24 def __init__(self, repository, revision):
25 self._stat_modes = {}
26 self.repository = repository
27 self.raw_id = revision
28 self.revision = repository.revisions.index(revision)
29
30 self.short_id = self.raw_id[:12]
31 self.id = self.raw_id
32 try:
33 commit = self.repository._repo.get_object(self.raw_id)
34 except KeyError:
35 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
36 self._commit = commit
37 self._tree_id = commit.tree
38
39 try:
40 self.message = safe_unicode(commit.message[:-1])
41 # Always strip last eol
42 except UnicodeDecodeError:
43 self.message = commit.message[:-1].decode(commit.encoding
44 or 'utf-8')
45 #self.branch = None
46 self.tags = []
47 #tree = self.repository.get_object(self._tree_id)
48 self.nodes = {}
49 self._paths = {}
50
51 @LazyProperty
52 def author(self):
53 return safe_unicode(self._commit.committer)
54
55 @LazyProperty
56 def date(self):
57 return date_fromtimestamp(self._commit.commit_time,
58 self._commit.commit_timezone)
59
60 @LazyProperty
61 def status(self):
62 """
63 Returns modified, added, removed, deleted files for current changeset
64 """
65 return self.changed, self.added, self.removed
66
67 @LazyProperty
68 def branch(self):
69 # TODO: Cache as we walk (id <-> branch name mapping)
70 refs = self.repository._repo.get_refs()
71 heads = [(key[len('refs/heads/'):], val) for key, val in refs.items()
72 if key.startswith('refs/heads/')]
73
74 for name, id in heads:
75 walker = self.repository._repo.object_store.get_graph_walker([id])
76 while True:
77 id = walker.next()
78 if not id:
79 break
80 if id == self.id:
81 return safe_unicode(name)
82 raise ChangesetError("This should not happen... Have you manually "
83 "change id of the changeset?")
84
85 def _fix_path(self, path):
86 """
87 Paths are stored without trailing slash so we need to get rid off it if
88 needed.
89 """
90 if path.endswith('/'):
91 path = path.rstrip('/')
92 return path
93
94 def _get_id_for_path(self, path):
95 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
96 if not path in self._paths:
97 path = path.strip('/')
98 # set root tree
99 tree = self.repository._repo[self._commit.tree]
100 if path == '':
101 self._paths[''] = tree.id
102 return tree.id
103 splitted = path.split('/')
104 dirs, name = splitted[:-1], splitted[-1]
105 curdir = ''
106 for dir in dirs:
107 if curdir:
108 curdir = '/'.join((curdir, dir))
109 else:
110 curdir = dir
111 #if curdir in self._paths:
112 ## This path have been already traversed
113 ## Update tree and continue
114 #tree = self.repository._repo[self._paths[curdir]]
115 #continue
116 dir_id = None
117 for item, stat, id in tree.iteritems():
118 if curdir:
119 item_path = '/'.join((curdir, item))
120 else:
121 item_path = item
122 self._paths[item_path] = id
123 self._stat_modes[item_path] = stat
124 if dir == item:
125 dir_id = id
126 if dir_id:
127 # Update tree
128 tree = self.repository._repo[dir_id]
129 if not isinstance(tree, objects.Tree):
130 raise ChangesetError('%s is not a directory' % curdir)
131 else:
132 raise ChangesetError('%s have not been found' % curdir)
133 for item, stat, id in tree.iteritems():
134 if curdir:
135 name = '/'.join((curdir, item))
136 else:
137 name = item
138 self._paths[name] = id
139 self._stat_modes[name] = stat
140 if not path in self._paths:
141 raise NodeDoesNotExistError("There is no file nor directory "
142 "at the given path %r at revision %r"
143 % (path, self.short_id))
144 return self._paths[path]
145
146 def _get_kind(self, path):
147 id = self._get_id_for_path(path)
148 obj = self.repository._repo[id]
149 if isinstance(obj, objects.Blob):
150 return NodeKind.FILE
151 elif isinstance(obj, objects.Tree):
152 return NodeKind.DIR
153
154 def _get_file_nodes(self):
155 return chain(*(t[2] for t in self.walk()))
156
157 @LazyProperty
158 def parents(self):
159 """
160 Returns list of parents changesets.
161 """
162 return [self.repository.get_changeset(parent)
163 for parent in self._commit.parents]
164
165 def next(self, branch=None):
166
167 if branch and self.branch != branch:
168 raise VCSError('Branch option used on changeset not belonging '
169 'to that branch')
170
171 def _next(changeset, branch):
172 try:
173 next_ = changeset.revision + 1
174 next_rev = changeset.repository.revisions[next_]
175 except IndexError:
176 raise ChangesetDoesNotExistError
177 cs = changeset.repository.get_changeset(next_rev)
178
179 if branch and branch != cs.branch:
180 return _next(cs, branch)
181
182 return cs
183
184 return _next(self, branch)
185
186 def prev(self, branch=None):
187 if branch and self.branch != branch:
188 raise VCSError('Branch option used on changeset not belonging '
189 'to that branch')
190
191 def _prev(changeset, branch):
192 try:
193 prev_ = changeset.revision - 1
194 if prev_ < 0:
195 raise IndexError
196 prev_rev = changeset.repository.revisions[prev_]
197 except IndexError:
198 raise ChangesetDoesNotExistError
199
200 cs = changeset.repository.get_changeset(prev_rev)
201
202 if branch and branch != cs.branch:
203 return _prev(cs, branch)
204
205 return cs
206
207 return _prev(self, branch)
208
209 def get_file_mode(self, path):
210 """
211 Returns stat mode of the file at the given ``path``.
212 """
213 # ensure path is traversed
214 self._get_id_for_path(path)
215 return self._stat_modes[path]
216
217 def get_file_content(self, path):
218 """
219 Returns content of the file at given ``path``.
220 """
221 id = self._get_id_for_path(path)
222 blob = self.repository._repo[id]
223 return blob.as_pretty_string()
224
225 def get_file_size(self, path):
226 """
227 Returns size of the file at given ``path``.
228 """
229 id = self._get_id_for_path(path)
230 blob = self.repository._repo[id]
231 return blob.raw_length()
232
233 def get_file_changeset(self, path):
234 """
235 Returns last commit of the file at the given ``path``.
236 """
237 node = self.get_node(path)
238 return node.history[0]
239
240 def get_file_history(self, path):
241 """
242 Returns history of file as reversed list of ``Changeset`` objects for
243 which file at given ``path`` has been modified.
244
245 TODO: This function now uses os underlying 'git' and 'grep' commands
246 which is generally not good. Should be replaced with algorithm
247 iterating commits.
248 """
249 cmd = 'log --name-status -p %s -- "%s" | grep "^commit"' \
250 % (self.id, path)
251 so, se = self.repository.run_git_command(cmd)
252 ids = re.findall(r'\w{40}', so)
253 return [self.repository.get_changeset(id) for id in ids]
254
255 def get_file_annotate(self, path):
256 """
257 Returns a list of three element tuples with lineno,changeset and line
258
259 TODO: This function now uses os underlying 'git' command which is
260 generally not good. Should be replaced with algorithm iterating
261 commits.
262 """
263 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
264 # -l ==> outputs long shas (and we need all 40 characters)
265 # --root ==> doesn't put '^' character for bounderies
266 # -r sha ==> blames for the given revision
267 so, se = self.repository.run_git_command(cmd)
268 annotate = []
269 for i, blame_line in enumerate(so.split('\n')[:-1]):
270 ln_no = i + 1
271 id, line = re.split(r' \(.+?\) ', blame_line, 1)
272 annotate.append((ln_no, self.repository.get_changeset(id), line))
273 return annotate
274
275 def fill_archive(self, stream=None, kind='tgz', prefix=None,
276 subrepos=False):
277 """
278 Fills up given stream.
279
280 :param stream: file like object.
281 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
282 Default: ``tgz``.
283 :param prefix: name of root directory in archive.
284 Default is repository name and changeset's raw_id joined with dash
285 (``repo-tip.<KIND>``).
286 :param subrepos: include subrepos in this archive.
287
288 :raise ImproperArchiveTypeError: If given kind is wrong.
289 :raise VcsError: If given stream is None
290
291 """
292 allowed_kinds = settings.ARCHIVE_SPECS.keys()
293 if kind not in allowed_kinds:
294 raise ImproperArchiveTypeError('Archive kind not supported use one'
295 'of %s', allowed_kinds)
296
297 if prefix is None:
298 prefix = '%s-%s' % (self.repository.name, self.short_id)
299 elif prefix.startswith('/'):
300 raise VCSError("Prefix cannot start with leading slash")
301 elif prefix.strip() == '':
302 raise VCSError("Prefix cannot be empty")
303
304 if kind == 'zip':
305 frmt = 'zip'
306 else:
307 frmt = 'tar'
308 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
309 self.raw_id)
310 if kind == 'tgz':
311 cmd += ' | gzip -9'
312 elif kind == 'tbz2':
313 cmd += ' | bzip2 -9'
314
315 if stream is None:
316 raise VCSError('You need to pass in a valid stream for filling'
317 ' with archival data')
318 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
319 cwd=self.repository.path)
320
321 buffer_size = 1024 * 8
322 chunk = popen.stdout.read(buffer_size)
323 while chunk:
324 stream.write(chunk)
325 chunk = popen.stdout.read(buffer_size)
326 # Make sure all descriptors would be read
327 popen.communicate()
328
329 def get_nodes(self, path):
330 if self._get_kind(path) != NodeKind.DIR:
331 raise ChangesetError("Directory does not exist for revision %r at "
332 " %r" % (self.revision, path))
333 path = self._fix_path(path)
334 id = self._get_id_for_path(path)
335 tree = self.repository._repo[id]
336 dirnodes = []
337 filenodes = []
338 for name, stat, id in tree.iteritems():
339 obj = self.repository._repo.get_object(id)
340 if path != '':
341 obj_path = '/'.join((path, name))
342 else:
343 obj_path = name
344 if obj_path not in self._stat_modes:
345 self._stat_modes[obj_path] = stat
346 if isinstance(obj, objects.Tree):
347 dirnodes.append(DirNode(obj_path, changeset=self))
348 elif isinstance(obj, objects.Blob):
349 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
350 else:
351 raise ChangesetError("Requested object should be Tree "
352 "or Blob, is %r" % type(obj))
353 nodes = dirnodes + filenodes
354 for node in nodes:
355 if not node.path in self.nodes:
356 self.nodes[node.path] = node
357 nodes.sort()
358 return nodes
359
360 def get_node(self, path):
361 if isinstance(path, unicode):
362 path = path.encode('utf-8')
363 path = self._fix_path(path)
364 if not path in self.nodes:
365 try:
366 id = self._get_id_for_path(path)
367 except ChangesetError:
368 raise NodeDoesNotExistError("Cannot find one of parents' "
369 "directories for a given path: %s" % path)
370 obj = self.repository._repo.get_object(id)
371 if isinstance(obj, objects.Tree):
372 if path == '':
373 node = RootNode(changeset=self)
374 else:
375 node = DirNode(path, changeset=self)
376 node._tree = obj
377 elif isinstance(obj, objects.Blob):
378 node = FileNode(path, changeset=self)
379 node._blob = obj
380 else:
381 raise NodeDoesNotExistError("There is no file nor directory "
382 "at the given path %r at revision %r"
383 % (path, self.short_id))
384 # cache node
385 self.nodes[path] = node
386 return self.nodes[path]
387
388 @LazyProperty
389 def affected_files(self):
390 """
391 Get's a fast accessible file changes for given changeset
392 """
393
394 return self.added + self.changed
395
396 @LazyProperty
397 def _diff_name_status(self):
398 output = []
399 for parent in self.parents:
400 cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id)
401 so, se = self.repository.run_git_command(cmd)
402 output.append(so.strip())
403 return '\n'.join(output)
404
405 def _get_paths_for_status(self, status):
406 """
407 Returns sorted list of paths for given ``status``.
408
409 :param status: one of: *added*, *modified* or *deleted*
410 """
411 paths = set()
412 char = status[0].upper()
413 for line in self._diff_name_status.splitlines():
414 if not line:
415 continue
416 if line.startswith(char):
417 splitted = line.split(char,1)
418 if not len(splitted) == 2:
419 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
420 "particularly that line: %s" % (self._diff_name_status,
421 line))
422 paths.add(splitted[1].strip())
423 return sorted(paths)
424
425 @LazyProperty
426 def added(self):
427 """
428 Returns list of added ``FileNode`` objects.
429 """
430 if not self.parents:
431 return list(self._get_file_nodes())
432 return [self.get_node(path) for path in self._get_paths_for_status('added')]
433
434 @LazyProperty
435 def changed(self):
436 """
437 Returns list of modified ``FileNode`` objects.
438 """
439 if not self.parents:
440 return []
441 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
442
443 @LazyProperty
444 def removed(self):
445 """
446 Returns list of removed ``FileNode`` objects.
447 """
448 if not self.parents:
449 return []
450 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -0,0 +1,347 b''
1 # config.py - Reading and writing Git config files
2 # Copyright (C) 2011 Jelmer Vernooij <jelmer@samba.org>
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; version 2
7 # of the License or (at your option) a later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17 # MA 02110-1301, USA.
18
19 """Reading and writing Git configuration files.
20
21 TODO:
22 * preserve formatting when updating configuration files
23 * treat subsection names as case-insensitive for [branch.foo] style
24 subsections
25 """
26
27 # Taken from dulwich not yet released 0.8.3 version (until it is actually
28 # released)
29
30 import errno
31 import os
32 import re
33
34 from dulwich.file import GitFile
35
36
37 class Config(object):
38 """A Git configuration."""
39
40 def get(self, section, name):
41 """Retrieve the contents of a configuration setting.
42
43 :param section: Tuple with section name and optional subsection namee
44 :param subsection: Subsection name
45 :return: Contents of the setting
46 :raise KeyError: if the value is not set
47 """
48 raise NotImplementedError(self.get)
49
50 def get_boolean(self, section, name, default=None):
51 """Retrieve a configuration setting as boolean.
52
53 :param section: Tuple with section name and optional subsection namee
54 :param name: Name of the setting, including section and possible
55 subsection.
56 :return: Contents of the setting
57 :raise KeyError: if the value is not set
58 """
59 try:
60 value = self.get(section, name)
61 except KeyError:
62 return default
63 if value.lower() == "true":
64 return True
65 elif value.lower() == "false":
66 return False
67 raise ValueError("not a valid boolean string: %r" % value)
68
69 def set(self, section, name, value):
70 """Set a configuration value.
71
72 :param name: Name of the configuration value, including section
73 and optional subsection
74 :param: Value of the setting
75 """
76 raise NotImplementedError(self.set)
77
78
79 class ConfigDict(Config):
80 """Git configuration stored in a dictionary."""
81
82 def __init__(self, values=None):
83 """Create a new ConfigDict."""
84 if values is None:
85 values = {}
86 self._values = values
87
88 def __repr__(self):
89 return "%s(%r)" % (self.__class__.__name__, self._values)
90
91 def __eq__(self, other):
92 return (
93 isinstance(other, self.__class__) and
94 other._values == self._values)
95
96 @classmethod
97 def _parse_setting(cls, name):
98 parts = name.split(".")
99 if len(parts) == 3:
100 return (parts[0], parts[1], parts[2])
101 else:
102 return (parts[0], None, parts[1])
103
104 def get(self, section, name):
105 if isinstance(section, basestring):
106 section = (section, )
107 if len(section) > 1:
108 try:
109 return self._values[section][name]
110 except KeyError:
111 pass
112 return self._values[(section[0],)][name]
113
114 def set(self, section, name, value):
115 if isinstance(section, basestring):
116 section = (section, )
117 self._values.setdefault(section, {})[name] = value
118
119
120 def _format_string(value):
121 if (value.startswith(" ") or
122 value.startswith("\t") or
123 value.endswith(" ") or
124 value.endswith("\t")):
125 return '"%s"' % _escape_value(value)
126 return _escape_value(value)
127
128
129 def _parse_string(value):
130 value = value.strip()
131 ret = []
132 block = []
133 in_quotes = False
134 for c in value:
135 if c == "\"":
136 in_quotes = (not in_quotes)
137 ret.append(_unescape_value("".join(block)))
138 block = []
139 elif c in ("#", ";") and not in_quotes:
140 # the rest of the line is a comment
141 break
142 else:
143 block.append(c)
144
145 if in_quotes:
146 raise ValueError("value starts with quote but lacks end quote")
147
148 ret.append(_unescape_value("".join(block)).rstrip())
149
150 return "".join(ret)
151
152
153 def _unescape_value(value):
154 """Unescape a value."""
155 def unescape(c):
156 return {
157 "\\\\": "\\",
158 "\\\"": "\"",
159 "\\n": "\n",
160 "\\t": "\t",
161 "\\b": "\b",
162 }[c.group(0)]
163 return re.sub(r"(\\.)", unescape, value)
164
165
166 def _escape_value(value):
167 """Escape a value."""
168 return value.replace("\\", "\\\\").replace("\n", "\\n")\
169 .replace("\t", "\\t").replace("\"", "\\\"")
170
171
172 def _check_variable_name(name):
173 for c in name:
174 if not c.isalnum() and c != '-':
175 return False
176 return True
177
178
179 def _check_section_name(name):
180 for c in name:
181 if not c.isalnum() and c not in ('-', '.'):
182 return False
183 return True
184
185
186 def _strip_comments(line):
187 line = line.split("#")[0]
188 line = line.split(";")[0]
189 return line
190
191
192 class ConfigFile(ConfigDict):
193 """A Git configuration file, like .git/config or ~/.gitconfig.
194 """
195
196 @classmethod
197 def from_file(cls, f):
198 """Read configuration from a file-like object."""
199 ret = cls()
200 section = None
201 setting = None
202 for lineno, line in enumerate(f.readlines()):
203 line = line.lstrip()
204 if setting is None:
205 if _strip_comments(line).strip() == "":
206 continue
207 if line[0] == "[":
208 line = _strip_comments(line).rstrip()
209 if line[-1] != "]":
210 raise ValueError("expected trailing ]")
211 key = line.strip()
212 pts = key[1:-1].split(" ", 1)
213 pts[0] = pts[0].lower()
214 if len(pts) == 2:
215 if pts[1][0] != "\"" or pts[1][-1] != "\"":
216 raise ValueError(
217 "Invalid subsection " + pts[1])
218 else:
219 pts[1] = pts[1][1:-1]
220 if not _check_section_name(pts[0]):
221 raise ValueError("invalid section name %s" %
222 pts[0])
223 section = (pts[0], pts[1])
224 else:
225 if not _check_section_name(pts[0]):
226 raise ValueError("invalid section name %s" %
227 pts[0])
228 pts = pts[0].split(".", 1)
229 if len(pts) == 2:
230 section = (pts[0], pts[1])
231 else:
232 section = (pts[0], )
233 ret._values[section] = {}
234 else:
235 if section is None:
236 raise ValueError("setting %r without section" % line)
237 try:
238 setting, value = line.split("=", 1)
239 except ValueError:
240 setting = line
241 value = "true"
242 setting = setting.strip().lower()
243 if not _check_variable_name(setting):
244 raise ValueError("invalid variable name %s" % setting)
245 if value.endswith("\\\n"):
246 value = value[:-2]
247 continuation = True
248 else:
249 continuation = False
250 value = _parse_string(value)
251 ret._values[section][setting] = value
252 if not continuation:
253 setting = None
254 else: # continuation line
255 if line.endswith("\\\n"):
256 line = line[:-2]
257 continuation = True
258 else:
259 continuation = False
260 value = _parse_string(line)
261 ret._values[section][setting] += value
262 if not continuation:
263 setting = None
264 return ret
265
266 @classmethod
267 def from_path(cls, path):
268 """Read configuration from a file on disk."""
269 f = GitFile(path, 'rb')
270 try:
271 ret = cls.from_file(f)
272 ret.path = path
273 return ret
274 finally:
275 f.close()
276
277 def write_to_path(self, path=None):
278 """Write configuration to a file on disk."""
279 if path is None:
280 path = self.path
281 f = GitFile(path, 'wb')
282 try:
283 self.write_to_file(f)
284 finally:
285 f.close()
286
287 def write_to_file(self, f):
288 """Write configuration to a file-like object."""
289 for section, values in self._values.iteritems():
290 try:
291 section_name, subsection_name = section
292 except ValueError:
293 (section_name, ) = section
294 subsection_name = None
295 if subsection_name is None:
296 f.write("[%s]\n" % section_name)
297 else:
298 f.write("[%s \"%s\"]\n" % (section_name, subsection_name))
299 for key, value in values.iteritems():
300 f.write("%s = %s\n" % (key, _escape_value(value)))
301
302
303 class StackedConfig(Config):
304 """Configuration which reads from multiple config files.."""
305
306 def __init__(self, backends, writable=None):
307 self.backends = backends
308 self.writable = writable
309
310 def __repr__(self):
311 return "<%s for %r>" % (self.__class__.__name__, self.backends)
312
313 @classmethod
314 def default_backends(cls):
315 """Retrieve the default configuration.
316
317 This will look in the repository configuration (if for_path is
318 specified), the users' home directory and the system
319 configuration.
320 """
321 paths = []
322 paths.append(os.path.expanduser("~/.gitconfig"))
323 paths.append("/etc/gitconfig")
324 backends = []
325 for path in paths:
326 try:
327 cf = ConfigFile.from_path(path)
328 except (IOError, OSError), e:
329 if e.errno != errno.ENOENT:
330 raise
331 else:
332 continue
333 backends.append(cf)
334 return backends
335
336 def get(self, section, name):
337 for backend in self.backends:
338 try:
339 return backend.get(section, name)
340 except KeyError:
341 pass
342 raise KeyError(name)
343
344 def set(self, section, name, value):
345 if self.writable is None:
346 raise NotImplementedError(self.set)
347 return self.writable.set(section, name, value)
@@ -0,0 +1,192 b''
1 import time
2 import datetime
3 import posixpath
4 from dulwich import objects
5 from dulwich.repo import Repo
6 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
7 from rhodecode.lib.vcs.exceptions import RepositoryError
8
9
10 class GitInMemoryChangeset(BaseInMemoryChangeset):
11
12 def commit(self, message, author, parents=None, branch=None, date=None,
13 **kwargs):
14 """
15 Performs in-memory commit (doesn't check workdir in any way) and
16 returns newly created ``Changeset``. Updates repository's
17 ``revisions``.
18
19 :param message: message of the commit
20 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
21 :param parents: single parent or sequence of parents from which commit
22 would be derieved
23 :param date: ``datetime.datetime`` instance. Defaults to
24 ``datetime.datetime.now()``.
25 :param branch: branch name, as string. If none given, default backend's
26 branch would be used.
27
28 :raises ``CommitError``: if any error occurs while committing
29 """
30 self.check_integrity(parents)
31
32 from .repository import GitRepository
33 if branch is None:
34 branch = GitRepository.DEFAULT_BRANCH_NAME
35
36 repo = self.repository._repo
37 object_store = repo.object_store
38
39 ENCODING = "UTF-8"
40 DIRMOD = 040000
41
42 # Create tree and populates it with blobs
43 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or\
44 objects.Tree()
45 for node in self.added + self.changed:
46 # Compute subdirs if needed
47 dirpath, nodename = posixpath.split(node.path)
48 dirnames = dirpath and dirpath.split('/') or []
49 parent = commit_tree
50 ancestors = [('', parent)]
51
52 # Tries to dig for the deepest existing tree
53 while dirnames:
54 curdir = dirnames.pop(0)
55 try:
56 dir_id = parent[curdir][1]
57 except KeyError:
58 # put curdir back into dirnames and stops
59 dirnames.insert(0, curdir)
60 break
61 else:
62 # If found, updates parent
63 parent = self.repository._repo[dir_id]
64 ancestors.append((curdir, parent))
65 # Now parent is deepest exising tree and we need to create subtrees
66 # for dirnames (in reverse order) [this only applies for nodes from added]
67 new_trees = []
68 blob = objects.Blob.from_string(node.content.encode(ENCODING))
69 node_path = node.name.encode(ENCODING)
70 if dirnames:
71 # If there are trees which should be created we need to build
72 # them now (in reverse order)
73 reversed_dirnames = list(reversed(dirnames))
74 curtree = objects.Tree()
75 curtree[node_path] = node.mode, blob.id
76 new_trees.append(curtree)
77 for dirname in reversed_dirnames[:-1]:
78 newtree = objects.Tree()
79 #newtree.add(DIRMOD, dirname, curtree.id)
80 newtree[dirname] = DIRMOD, curtree.id
81 new_trees.append(newtree)
82 curtree = newtree
83 parent[reversed_dirnames[-1]] = DIRMOD, curtree.id
84 else:
85 parent.add(node.mode, node_path, blob.id)
86 new_trees.append(parent)
87 # Update ancestors
88 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
89 zip(ancestors, ancestors[1:])]):
90 parent[path] = DIRMOD, tree.id
91 object_store.add_object(tree)
92
93 object_store.add_object(blob)
94 for tree in new_trees:
95 object_store.add_object(tree)
96 for node in self.removed:
97 paths = node.path.split('/')
98 tree = commit_tree
99 trees = [tree]
100 # Traverse deep into the forest...
101 for path in paths:
102 try:
103 obj = self.repository._repo[tree[path][1]]
104 if isinstance(obj, objects.Tree):
105 trees.append(obj)
106 tree = obj
107 except KeyError:
108 break
109 # Cut down the blob and all rotten trees on the way back...
110 for path, tree in reversed(zip(paths, trees)):
111 del tree[path]
112 if tree:
113 # This tree still has elements - don't remove it or any
114 # of it's parents
115 break
116
117 object_store.add_object(commit_tree)
118
119 # Create commit
120 commit = objects.Commit()
121 commit.tree = commit_tree.id
122 commit.parents = [p._commit.id for p in self.parents if p]
123 commit.author = commit.committer = author
124 commit.encoding = ENCODING
125 commit.message = message + ' '
126
127 # Compute date
128 if date is None:
129 date = time.time()
130 elif isinstance(date, datetime.datetime):
131 date = time.mktime(date.timetuple())
132
133 author_time = kwargs.pop('author_time', date)
134 commit.commit_time = int(date)
135 commit.author_time = int(author_time)
136 tz = time.timezone
137 author_tz = kwargs.pop('author_timezone', tz)
138 commit.commit_timezone = tz
139 commit.author_timezone = author_tz
140
141 object_store.add_object(commit)
142
143 ref = 'refs/heads/%s' % branch
144 repo.refs[ref] = commit.id
145 repo.refs.set_symbolic_ref('HEAD', ref)
146
147 # Update vcs repository object & recreate dulwich repo
148 self.repository.revisions.append(commit.id)
149 self.repository._repo = Repo(self.repository.path)
150 tip = self.repository.get_changeset()
151 self.reset()
152 return tip
153
154 def _get_missing_trees(self, path, root_tree):
155 """
156 Creates missing ``Tree`` objects for the given path.
157
158 :param path: path given as a string. It may be a path to a file node
159 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
160 end with slash (i.e. ``foo/bar/``).
161 :param root_tree: ``dulwich.objects.Tree`` object from which we start
162 traversing (should be commit's root tree)
163 """
164 dirpath = posixpath.split(path)[0]
165 dirs = dirpath.split('/')
166 if not dirs or dirs == ['']:
167 return []
168
169 def get_tree_for_dir(tree, dirname):
170 for name, mode, id in tree.iteritems():
171 if name == dirname:
172 obj = self.repository._repo[id]
173 if isinstance(obj, objects.Tree):
174 return obj
175 else:
176 raise RepositoryError("Cannot create directory %s "
177 "at tree %s as path is occupied and is not a "
178 "Tree" % (dirname, tree))
179 return None
180
181 trees = []
182 parent = root_tree
183 for dirname in dirs:
184 tree = get_tree_for_dir(parent, dirname)
185 if tree is None:
186 tree = objects.Tree()
187 dirmode = 040000
188 parent.add(dirmode, dirname, tree.id)
189 parent = tree
190 # Always append tree
191 trees.append(tree)
192 return trees
This diff has been collapsed as it changes many lines, (508 lines changed) Show them Hide them
@@ -0,0 +1,508 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs.backends.git
4 ~~~~~~~~~~~~~~~~
5
6 Git backend implementation.
7
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
11
12 import os
13 import re
14 import time
15 import posixpath
16 from dulwich.repo import Repo, NotGitRepository
17 #from dulwich.config import ConfigFile
18 from string import Template
19 from subprocess import Popen, PIPE
20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 from rhodecode.lib.vcs.utils.paths import abspath
31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 from .workdir import GitWorkdir
33 from .changeset import GitChangeset
34 from .inmemory import GitInMemoryChangeset
35 from .config import ConfigFile
36
37
38 class GitRepository(BaseRepository):
39 """
40 Git repository backend.
41 """
42 DEFAULT_BRANCH_NAME = 'master'
43 scm = 'git'
44
45 def __init__(self, repo_path, create=False, src_url=None,
46 update_after_clone=False, bare=False):
47
48 self.path = abspath(repo_path)
49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 try:
51 self.head = self._repo.head()
52 except KeyError:
53 self.head = None
54
55 self._config_files = [
56 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
57 'config'),
58 abspath(get_user_home(), '.gitconfig'),
59 ]
60
61 @LazyProperty
62 def revisions(self):
63 """
64 Returns list of revisions' ids, in ascending order. Being lazy
65 attribute allows external tools to inject shas from cache.
66 """
67 return self._get_all_revisions()
68
69 def run_git_command(self, cmd):
70 """
71 Runs given ``cmd`` as git command and returns tuple
72 (returncode, stdout, stderr).
73
74 .. note::
75 This method exists only until log/blame functionality is implemented
76 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
77 os command's output is road to hell...
78
79 :param cmd: git command to be executed
80 """
81 #cmd = '(cd %s && git %s)' % (self.path, cmd)
82 if isinstance(cmd, basestring):
83 cmd = 'git %s' % cmd
84 else:
85 cmd = ['git'] + cmd
86 try:
87 opts = dict(
88 shell=isinstance(cmd, basestring),
89 stdout=PIPE,
90 stderr=PIPE)
91 if os.path.isdir(self.path):
92 opts['cwd'] = self.path
93 p = Popen(cmd, **opts)
94 except OSError, err:
95 raise RepositoryError("Couldn't run git command (%s).\n"
96 "Original error was:%s" % (cmd, err))
97 so, se = p.communicate()
98 if not se.startswith("fatal: bad default revision 'HEAD'") and \
99 p.returncode != 0:
100 raise RepositoryError("Couldn't run git command (%s).\n"
101 "stderr:\n%s" % (cmd, se))
102 return so, se
103
104 def _check_url(self, url):
105 """
106 Functon will check given url and try to verify if it's a valid
107 link. Sometimes it may happened that mercurial will issue basic
108 auth request that can cause whole API to hang when used from python
109 or other external calls.
110
111 On failures it'll raise urllib2.HTTPError
112 """
113
114 #TODO: implement this
115 pass
116
117 def _get_repo(self, create, src_url=None, update_after_clone=False,
118 bare=False):
119 if create and os.path.exists(self.path):
120 raise RepositoryError("Location already exist")
121 if src_url and not create:
122 raise RepositoryError("Create should be set to True if src_url is "
123 "given (clone operation creates repository)")
124 try:
125 if create and src_url:
126 self._check_url(src_url)
127 self.clone(src_url, update_after_clone, bare)
128 return Repo(self.path)
129 elif create:
130 os.mkdir(self.path)
131 if bare:
132 return Repo.init_bare(self.path)
133 else:
134 return Repo.init(self.path)
135 else:
136 return Repo(self.path)
137 except (NotGitRepository, OSError), err:
138 raise RepositoryError(err)
139
140 def _get_all_revisions(self):
141 cmd = 'rev-list --all --date-order'
142 try:
143 so, se = self.run_git_command(cmd)
144 except RepositoryError:
145 # Can be raised for empty repositories
146 return []
147 revisions = so.splitlines()
148 revisions.reverse()
149 return revisions
150
151 def _get_revision(self, revision):
152 """
153 For git backend we always return integer here. This way we ensure
154 that changset's revision attribute would become integer.
155 """
156 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
157 is_bstr = lambda o: isinstance(o, (str, unicode))
158 is_null = lambda o: len(o) == revision.count('0')
159
160 if len(self.revisions) == 0:
161 raise EmptyRepositoryError("There are no changesets yet")
162
163 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
164 revision = self.revisions[-1]
165
166 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
167 or isinstance(revision, int) or is_null(revision)):
168 try:
169 revision = self.revisions[int(revision)]
170 except:
171 raise ChangesetDoesNotExistError("Revision %r does not exist "
172 "for this repository %s" % (revision, self))
173
174 elif is_bstr(revision):
175 if not pattern.match(revision) or revision not in self.revisions:
176 raise ChangesetDoesNotExistError("Revision %r does not exist "
177 "for this repository %s" % (revision, self))
178
179 # Ensure we return full id
180 if not pattern.match(str(revision)):
181 raise ChangesetDoesNotExistError("Given revision %r not recognized"
182 % revision)
183 return revision
184
185 def _get_archives(self, archive_name='tip'):
186
187 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
188 yield {"type": i[0], "extension": i[1], "node": archive_name}
189
190 def _get_url(self, url):
191 """
192 Returns normalized url. If schema is not given, would fall to
193 filesystem (``file:///``) schema.
194 """
195 url = str(url)
196 if url != 'default' and not '://' in url:
197 url = ':///'.join(('file', url))
198 return url
199
200 @LazyProperty
201 def name(self):
202 return os.path.basename(self.path)
203
204 @LazyProperty
205 def last_change(self):
206 """
207 Returns last change made on this repository as datetime object
208 """
209 return date_fromtimestamp(self._get_mtime(), makedate()[1])
210
211 def _get_mtime(self):
212 try:
213 return time.mktime(self.get_changeset().date.timetuple())
214 except RepositoryError:
215 # fallback to filesystem
216 in_path = os.path.join(self.path, '.git', "index")
217 he_path = os.path.join(self.path, '.git', "HEAD")
218 if os.path.exists(in_path):
219 return os.stat(in_path).st_mtime
220 else:
221 return os.stat(he_path).st_mtime
222
223 @LazyProperty
224 def description(self):
225 undefined_description = u'unknown'
226 description_path = os.path.join(self.path, '.git', 'description')
227 if os.path.isfile(description_path):
228 return safe_unicode(open(description_path).read())
229 else:
230 return undefined_description
231
232 @LazyProperty
233 def contact(self):
234 undefined_contact = u'Unknown'
235 return undefined_contact
236
237 @property
238 def branches(self):
239 if not self.revisions:
240 return {}
241 refs = self._repo.refs.as_dict()
242 sortkey = lambda ctx: ctx[0]
243 _branches = [('/'.join(ref.split('/')[2:]), head)
244 for ref, head in refs.items()
245 if ref.startswith('refs/heads/') or
246 ref.startswith('refs/remotes/') and not ref.endswith('/HEAD')]
247 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
248
249 def _get_tags(self):
250 if not self.revisions:
251 return {}
252 sortkey = lambda ctx: ctx[0]
253 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
254 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
255 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
256
257 @LazyProperty
258 def tags(self):
259 return self._get_tags()
260
261 def tag(self, name, user, revision=None, message=None, date=None,
262 **kwargs):
263 """
264 Creates and returns a tag for the given ``revision``.
265
266 :param name: name for new tag
267 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
268 :param revision: changeset id for which new tag would be created
269 :param message: message of the tag's commit
270 :param date: date of tag's commit
271
272 :raises TagAlreadyExistError: if tag with same name already exists
273 """
274 if name in self.tags:
275 raise TagAlreadyExistError("Tag %s already exists" % name)
276 changeset = self.get_changeset(revision)
277 message = message or "Added tag %s for commit %s" % (name,
278 changeset.raw_id)
279 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
280
281 self.tags = self._get_tags()
282 return changeset
283
284 def remove_tag(self, name, user, message=None, date=None):
285 """
286 Removes tag with the given ``name``.
287
288 :param name: name of the tag to be removed
289 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
290 :param message: message of the tag's removal commit
291 :param date: date of tag's removal commit
292
293 :raises TagDoesNotExistError: if tag with given name does not exists
294 """
295 if name not in self.tags:
296 raise TagDoesNotExistError("Tag %s does not exist" % name)
297 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
298 try:
299 os.remove(tagpath)
300 self.tags = self._get_tags()
301 except OSError, e:
302 raise RepositoryError(e.strerror)
303
304 def get_changeset(self, revision=None):
305 """
306 Returns ``GitChangeset`` object representing commit from git repository
307 at the given revision or head (most recent commit) if None given.
308 """
309 if isinstance(revision, GitChangeset):
310 return revision
311 revision = self._get_revision(revision)
312 changeset = GitChangeset(repository=self, revision=revision)
313 return changeset
314
315 def get_changesets(self, start=None, end=None, start_date=None,
316 end_date=None, branch_name=None, reverse=False):
317 """
318 Returns iterator of ``GitChangeset`` objects from start to end (both
319 are inclusive), in ascending date order (unless ``reverse`` is set).
320
321 :param start: changeset ID, as str; first returned changeset
322 :param end: changeset ID, as str; last returned changeset
323 :param start_date: if specified, changesets with commit date less than
324 ``start_date`` would be filtered out from returned set
325 :param end_date: if specified, changesets with commit date greater than
326 ``end_date`` would be filtered out from returned set
327 :param branch_name: if specified, changesets not reachable from given
328 branch would be filtered out from returned set
329 :param reverse: if ``True``, returned generator would be reversed
330 (meaning that returned changesets would have descending date order)
331
332 :raise BranchDoesNotExistError: If given ``branch_name`` does not
333 exist.
334 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
335 ``end`` could not be found.
336
337 """
338 if branch_name and branch_name not in self.branches:
339 raise BranchDoesNotExistError("Branch '%s' not found" \
340 % branch_name)
341 # %H at format means (full) commit hash, initial hashes are retrieved
342 # in ascending date order
343 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
344 cmd_params = {}
345 if start_date:
346 cmd_template += ' --since "$since"'
347 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
348 if end_date:
349 cmd_template += ' --until "$until"'
350 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
351 if branch_name:
352 cmd_template += ' $branch_name'
353 cmd_params['branch_name'] = branch_name
354 else:
355 cmd_template += ' --all'
356
357 cmd = Template(cmd_template).safe_substitute(**cmd_params)
358 revs = self.run_git_command(cmd)[0].splitlines()
359 start_pos = 0
360 end_pos = len(revs)
361 if start:
362 _start = self._get_revision(start)
363 try:
364 start_pos = revs.index(_start)
365 except ValueError:
366 pass
367
368 if end is not None:
369 _end = self._get_revision(end)
370 try:
371 end_pos = revs.index(_end)
372 except ValueError:
373 pass
374
375 if None not in [start, end] and start_pos > end_pos:
376 raise RepositoryError('start cannot be after end')
377
378 if end_pos is not None:
379 end_pos += 1
380
381 revs = revs[start_pos:end_pos]
382 if reverse:
383 revs = reversed(revs)
384 for rev in revs:
385 yield self.get_changeset(rev)
386
387 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
388 context=3):
389 """
390 Returns (git like) *diff*, as plain text. Shows changes introduced by
391 ``rev2`` since ``rev1``.
392
393 :param rev1: Entry point from which diff is shown. Can be
394 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
395 the changes since empty state of the repository until ``rev2``
396 :param rev2: Until which revision changes should be shown.
397 :param ignore_whitespace: If set to ``True``, would not show whitespace
398 changes. Defaults to ``False``.
399 :param context: How many lines before/after changed lines should be
400 shown. Defaults to ``3``.
401 """
402 flags = ['-U%s' % context]
403 if ignore_whitespace:
404 flags.append('-w')
405
406 if rev1 == self.EMPTY_CHANGESET:
407 rev2 = self.get_changeset(rev2).raw_id
408 cmd = ' '.join(['show'] + flags + [rev2])
409 else:
410 rev1 = self.get_changeset(rev1).raw_id
411 rev2 = self.get_changeset(rev2).raw_id
412 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
413
414 if path:
415 cmd += ' -- "%s"' % path
416 stdout, stderr = self.run_git_command(cmd)
417 # If we used 'show' command, strip first few lines (until actual diff
418 # starts)
419 if rev1 == self.EMPTY_CHANGESET:
420 lines = stdout.splitlines()
421 x = 0
422 for line in lines:
423 if line.startswith('diff'):
424 break
425 x += 1
426 # Append new line just like 'diff' command do
427 stdout = '\n'.join(lines[x:]) + '\n'
428 return stdout
429
430 @LazyProperty
431 def in_memory_changeset(self):
432 """
433 Returns ``GitInMemoryChangeset`` object for this repository.
434 """
435 return GitInMemoryChangeset(self)
436
437 def clone(self, url, update_after_clone=True, bare=False):
438 """
439 Tries to clone changes from external location.
440
441 :param update_after_clone: If set to ``False``, git won't checkout
442 working directory
443 :param bare: If set to ``True``, repository would be cloned into
444 *bare* git repository (no working directory at all).
445 """
446 url = self._get_url(url)
447 cmd = ['clone']
448 if bare:
449 cmd.append('--bare')
450 elif not update_after_clone:
451 cmd.append('--no-checkout')
452 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
453 cmd = ' '.join(cmd)
454 # If error occurs run_git_command raises RepositoryError already
455 self.run_git_command(cmd)
456
457 @LazyProperty
458 def workdir(self):
459 """
460 Returns ``Workdir`` instance for this repository.
461 """
462 return GitWorkdir(self)
463
464 def get_config_value(self, section, name, config_file=None):
465 """
466 Returns configuration value for a given [``section``] and ``name``.
467
468 :param section: Section we want to retrieve value from
469 :param name: Name of configuration we want to retrieve
470 :param config_file: A path to file which should be used to retrieve
471 configuration from (might also be a list of file paths)
472 """
473 if config_file is None:
474 config_file = []
475 elif isinstance(config_file, basestring):
476 config_file = [config_file]
477
478 def gen_configs():
479 for path in config_file + self._config_files:
480 try:
481 yield ConfigFile.from_path(path)
482 except (IOError, OSError, ValueError):
483 continue
484
485 for config in gen_configs():
486 try:
487 return config.get(section, name)
488 except KeyError:
489 continue
490 return None
491
492 def get_user_name(self, config_file=None):
493 """
494 Returns user's name from global configuration file.
495
496 :param config_file: A path to file which should be used to retrieve
497 configuration from (might also be a list of file paths)
498 """
499 return self.get_config_value('user', 'name', config_file)
500
501 def get_user_email(self, config_file=None):
502 """
503 Returns user's email from global configuration file.
504
505 :param config_file: A path to file which should be used to retrieve
506 configuration from (might also be a list of file paths)
507 """
508 return self.get_config_value('user', 'email', config_file)
@@ -0,0 +1,31 b''
1 import re
2 from rhodecode.lib.vcs.backends.base import BaseWorkdir
3 from rhodecode.lib.vcs.exceptions import RepositoryError
4 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
5
6
7 class GitWorkdir(BaseWorkdir):
8
9 def get_branch(self):
10 headpath = self.repository._repo.refs.refpath('HEAD')
11 try:
12 content = open(headpath).read()
13 match = re.match(r'^ref: refs/heads/(?P<branch>.+)\n$', content)
14 if match:
15 return match.groupdict()['branch']
16 else:
17 raise RepositoryError("Couldn't compute workdir's branch")
18 except IOError:
19 # Try naive way...
20 raise RepositoryError("Couldn't compute workdir's branch")
21
22 def get_changeset(self):
23 return self.repository.get_changeset(
24 self.repository._repo.refs.as_dict().get('HEAD'))
25
26 def checkout_branch(self, branch=None):
27 if branch is None:
28 branch = self.repository.DEFAULT_BRANCH_NAME
29 if branch not in self.repository.branches:
30 raise BranchDoesNotExistError
31 self.repository.run_git_command(['checkout', branch])
@@ -0,0 +1,21 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs.backends.hg
4 ~~~~~~~~~~~~~~~~
5
6 Mercurial backend implementation.
7
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
11
12 from .repository import MercurialRepository
13 from .changeset import MercurialChangeset
14 from .inmemory import MercurialInMemoryChangeset
15 from .workdir import MercurialWorkdir
16
17
18 __all__ = [
19 'MercurialRepository', 'MercurialChangeset',
20 'MercurialInMemoryChangeset', 'MercurialWorkdir',
21 ]
@@ -0,0 +1,338 b''
1 import os
2 import posixpath
3
4 from rhodecode.lib.vcs.backends.base import BaseChangeset
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, ChangedFileNodesGenerator, \
9 DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode
10
11 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
12 from rhodecode.lib.vcs.utils.lazy import LazyProperty
13 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
14
15 from ...utils.hgcompat import archival, hex
16
17
18 class MercurialChangeset(BaseChangeset):
19 """
20 Represents state of the repository at the single revision.
21 """
22
23 def __init__(self, repository, revision):
24 self.repository = repository
25 self.raw_id = revision
26 self._ctx = repository._repo[revision]
27 self.revision = self._ctx._rev
28 self.nodes = {}
29
30 @LazyProperty
31 def tags(self):
32 return map(safe_unicode, self._ctx.tags())
33
34 @LazyProperty
35 def branch(self):
36 return safe_unicode(self._ctx.branch())
37
38 @LazyProperty
39 def message(self):
40 return safe_unicode(self._ctx.description())
41
42 @LazyProperty
43 def author(self):
44 return safe_unicode(self._ctx.user())
45
46 @LazyProperty
47 def date(self):
48 return date_fromtimestamp(*self._ctx.date())
49
50 @LazyProperty
51 def status(self):
52 """
53 Returns modified, added, removed, deleted files for current changeset
54 """
55 return self.repository._repo.status(self._ctx.p1().node(),
56 self._ctx.node())
57
58 @LazyProperty
59 def _file_paths(self):
60 return list(self._ctx)
61
62 @LazyProperty
63 def _dir_paths(self):
64 p = list(set(get_dirs_for_path(*self._file_paths)))
65 p.insert(0, '')
66 return p
67
68 @LazyProperty
69 def _paths(self):
70 return self._dir_paths + self._file_paths
71
72 @LazyProperty
73 def id(self):
74 if self.last:
75 return u'tip'
76 return self.short_id
77
78 @LazyProperty
79 def short_id(self):
80 return self.raw_id[:12]
81
82 @LazyProperty
83 def parents(self):
84 """
85 Returns list of parents changesets.
86 """
87 return [self.repository.get_changeset(parent.rev())
88 for parent in self._ctx.parents() if parent.rev() >= 0]
89
90 def next(self, branch=None):
91
92 if branch and self.branch != branch:
93 raise VCSError('Branch option used on changeset not belonging '
94 'to that branch')
95
96 def _next(changeset, branch):
97 try:
98 next_ = changeset.revision + 1
99 next_rev = changeset.repository.revisions[next_]
100 except IndexError:
101 raise ChangesetDoesNotExistError
102 cs = changeset.repository.get_changeset(next_rev)
103
104 if branch and branch != cs.branch:
105 return _next(cs, branch)
106
107 return cs
108
109 return _next(self, branch)
110
111 def prev(self, branch=None):
112 if branch and self.branch != branch:
113 raise VCSError('Branch option used on changeset not belonging '
114 'to that branch')
115
116 def _prev(changeset, branch):
117 try:
118 prev_ = changeset.revision - 1
119 if prev_ < 0:
120 raise IndexError
121 prev_rev = changeset.repository.revisions[prev_]
122 except IndexError:
123 raise ChangesetDoesNotExistError
124
125 cs = changeset.repository.get_changeset(prev_rev)
126
127 if branch and branch != cs.branch:
128 return _prev(cs, branch)
129
130 return cs
131
132 return _prev(self, branch)
133
134 def _fix_path(self, path):
135 """
136 Paths are stored without trailing slash so we need to get rid off it if
137 needed. Also mercurial keeps filenodes as str so we need to decode
138 from unicode to str
139 """
140 if path.endswith('/'):
141 path = path.rstrip('/')
142
143 return safe_str(path)
144
145 def _get_kind(self, path):
146 path = self._fix_path(path)
147 if path in self._file_paths:
148 return NodeKind.FILE
149 elif path in self._dir_paths:
150 return NodeKind.DIR
151 else:
152 raise ChangesetError("Node does not exist at the given path %r"
153 % (path))
154
155 def _get_filectx(self, path):
156 path = self._fix_path(path)
157 if self._get_kind(path) != NodeKind.FILE:
158 raise ChangesetError("File does not exist for revision %r at "
159 " %r" % (self.revision, path))
160 return self._ctx.filectx(path)
161
162 def get_file_mode(self, path):
163 """
164 Returns stat mode of the file at the given ``path``.
165 """
166 fctx = self._get_filectx(path)
167 if 'x' in fctx.flags():
168 return 0100755
169 else:
170 return 0100644
171
172 def get_file_content(self, path):
173 """
174 Returns content of the file at given ``path``.
175 """
176 fctx = self._get_filectx(path)
177 return fctx.data()
178
179 def get_file_size(self, path):
180 """
181 Returns size of the file at given ``path``.
182 """
183 fctx = self._get_filectx(path)
184 return fctx.size()
185
186 def get_file_changeset(self, path):
187 """
188 Returns last commit of the file at the given ``path``.
189 """
190 fctx = self._get_filectx(path)
191 changeset = self.repository.get_changeset(fctx.linkrev())
192 return changeset
193
194 def get_file_history(self, path):
195 """
196 Returns history of file as reversed list of ``Changeset`` objects for
197 which file at given ``path`` has been modified.
198 """
199 fctx = self._get_filectx(path)
200 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
201 changesets = [self.repository.get_changeset(hex(node))
202 for node in reversed(nodes)]
203 return changesets
204
205 def get_file_annotate(self, path):
206 """
207 Returns a list of three element tuples with lineno,changeset and line
208 """
209 fctx = self._get_filectx(path)
210 annotate = []
211 for i, annotate_data in enumerate(fctx.annotate()):
212 ln_no = i + 1
213 annotate.append((ln_no, self.repository\
214 .get_changeset(hex(annotate_data[0].node())),
215 annotate_data[1],))
216
217 return annotate
218
219 def fill_archive(self, stream=None, kind='tgz', prefix=None,
220 subrepos=False):
221 """
222 Fills up given stream.
223
224 :param stream: file like object.
225 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
226 Default: ``tgz``.
227 :param prefix: name of root directory in archive.
228 Default is repository name and changeset's raw_id joined with dash
229 (``repo-tip.<KIND>``).
230 :param subrepos: include subrepos in this archive.
231
232 :raise ImproperArchiveTypeError: If given kind is wrong.
233 :raise VcsError: If given stream is None
234 """
235
236 allowed_kinds = settings.ARCHIVE_SPECS.keys()
237 if kind not in allowed_kinds:
238 raise ImproperArchiveTypeError('Archive kind not supported use one'
239 'of %s', allowed_kinds)
240
241 if stream is None:
242 raise VCSError('You need to pass in a valid stream for filling'
243 ' with archival data')
244
245 if prefix is None:
246 prefix = '%s-%s' % (self.repository.name, self.short_id)
247 elif prefix.startswith('/'):
248 raise VCSError("Prefix cannot start with leading slash")
249 elif prefix.strip() == '':
250 raise VCSError("Prefix cannot be empty")
251
252 archival.archive(self.repository._repo, stream, self.raw_id,
253 kind, prefix=prefix, subrepos=subrepos)
254
255 #stream.close()
256
257 if stream.closed and hasattr(stream, 'name'):
258 stream = open(stream.name, 'rb')
259 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
260 stream = open(stream.name, 'rb')
261 else:
262 stream.seek(0)
263
264 def get_nodes(self, path):
265 """
266 Returns combined ``DirNode`` and ``FileNode`` objects list representing
267 state of changeset at the given ``path``. If node at the given ``path``
268 is not instance of ``DirNode``, ChangesetError would be raised.
269 """
270
271 if self._get_kind(path) != NodeKind.DIR:
272 raise ChangesetError("Directory does not exist for revision %r at "
273 " %r" % (self.revision, path))
274 path = self._fix_path(path)
275 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
276 if os.path.dirname(f) == path]
277 dirs = path == '' and '' or [d for d in self._dir_paths
278 if d and posixpath.dirname(d) == path]
279 dirnodes = [DirNode(d, changeset=self) for d in dirs
280 if os.path.dirname(d) == path]
281 nodes = dirnodes + filenodes
282 # cache nodes
283 for node in nodes:
284 self.nodes[node.path] = node
285 nodes.sort()
286 return nodes
287
288 def get_node(self, path):
289 """
290 Returns ``Node`` object from the given ``path``. If there is no node at
291 the given ``path``, ``ChangesetError`` would be raised.
292 """
293
294 path = self._fix_path(path)
295
296 if not path in self.nodes:
297 if path in self._file_paths:
298 node = FileNode(path, changeset=self)
299 elif path in self._dir_paths or path in self._dir_paths:
300 if path == '':
301 node = RootNode(changeset=self)
302 else:
303 node = DirNode(path, changeset=self)
304 else:
305 raise NodeDoesNotExistError("There is no file nor directory "
306 "at the given path: %r at revision %r"
307 % (path, self.short_id))
308 # cache node
309 self.nodes[path] = node
310 return self.nodes[path]
311
312 @LazyProperty
313 def affected_files(self):
314 """
315 Get's a fast accessible file changes for given changeset
316 """
317 return self._ctx.files()
318
319 @property
320 def added(self):
321 """
322 Returns list of added ``FileNode`` objects.
323 """
324 return AddedFileNodesGenerator([n for n in self.status[1]], self)
325
326 @property
327 def changed(self):
328 """
329 Returns list of modified ``FileNode`` objects.
330 """
331 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
332
333 @property
334 def removed(self):
335 """
336 Returns list of removed ``FileNode`` objects.
337 """
338 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -0,0 +1,110 b''
1 import datetime
2 import errno
3
4 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
5 from rhodecode.lib.vcs.exceptions import RepositoryError
6
7 from ...utils.hgcompat import memfilectx, memctx, hex
8
9
10 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
11
12 def commit(self, message, author, parents=None, branch=None, date=None,
13 **kwargs):
14 """
15 Performs in-memory commit (doesn't check workdir in any way) and
16 returns newly created ``Changeset``. Updates repository's
17 ``revisions``.
18
19 :param message: message of the commit
20 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
21 :param parents: single parent or sequence of parents from which commit
22 would be derieved
23 :param date: ``datetime.datetime`` instance. Defaults to
24 ``datetime.datetime.now()``.
25 :param branch: branch name, as string. If none given, default backend's
26 branch would be used.
27
28 :raises ``CommitError``: if any error occurs while committing
29 """
30 self.check_integrity(parents)
31
32 from .repository import MercurialRepository
33 if not isinstance(message, str) or not isinstance(author, str):
34 raise RepositoryError('Given message and author needs to be '
35 'an <str> instance')
36
37 if branch is None:
38 branch = MercurialRepository.DEFAULT_BRANCH_NAME
39 kwargs['branch'] = branch
40
41 def filectxfn(_repo, memctx, path):
42 """
43 Marks given path as added/changed/removed in a given _repo. This is
44 for internal mercurial commit function.
45 """
46
47 # check if this path is removed
48 if path in (node.path for node in self.removed):
49 # Raising exception is a way to mark node for removal
50 raise IOError(errno.ENOENT, '%s is deleted' % path)
51
52 # check if this path is added
53 for node in self.added:
54 if node.path == path:
55 return memfilectx(path=node.path,
56 data=(node.content.encode('utf8')
57 if not node.is_binary else node.content),
58 islink=False,
59 isexec=node.is_executable,
60 copied=False)
61
62 # or changed
63 for node in self.changed:
64 if node.path == path:
65 return memfilectx(path=node.path,
66 data=(node.content.encode('utf8')
67 if not node.is_binary else node.content),
68 islink=False,
69 isexec=node.is_executable,
70 copied=False)
71
72 raise RepositoryError("Given path haven't been marked as added,"
73 "changed or removed (%s)" % path)
74
75 parents = [None, None]
76 for i, parent in enumerate(self.parents):
77 if parent is not None:
78 parents[i] = parent._ctx.node()
79
80 if date and isinstance(date, datetime.datetime):
81 date = date.ctime()
82
83 commit_ctx = memctx(repo=self.repository._repo,
84 parents=parents,
85 text='',
86 files=self.get_paths(),
87 filectxfn=filectxfn,
88 user=author,
89 date=date,
90 extra=kwargs)
91
92 # injecting given _repo params
93 commit_ctx._text = message
94 commit_ctx._user = author
95 commit_ctx._date = date
96
97 # TODO: Catch exceptions!
98 n = self.repository._repo.commitctx(commit_ctx)
99 # Returns mercurial node
100 self._commit_ctx = commit_ctx # For reference
101 # Update vcs repository object & recreate mercurial _repo
102 # new_ctx = self.repository._repo[node]
103 # new_tip = self.repository.get_changeset(new_ctx.hex())
104 new_id = hex(n)
105 self.repository.revisions.append(new_id)
106 self._repo = self.repository._get_repo(create=False)
107 self.repository.branches = self.repository._get_branches()
108 tip = self.repository.get_changeset()
109 self.reset()
110 return tip
This diff has been collapsed as it changes many lines, (521 lines changed) Show them Hide them
@@ -0,0 +1,521 b''
1 import os
2 import time
3 import datetime
4 import urllib
5 import urllib2
6
7 from rhodecode.lib.vcs.backends.base import BaseRepository
8 from .workdir import MercurialWorkdir
9 from .changeset import MercurialChangeset
10 from .inmemory import MercurialInMemoryChangeset
11
12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
14 VCSError, TagAlreadyExistError, TagDoesNotExistError
15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
16 makedate, safe_unicode
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
19 from rhodecode.lib.vcs.utils.paths import abspath
20
21 from ...utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
23
24
25 class MercurialRepository(BaseRepository):
26 """
27 Mercurial repository backend
28 """
29 DEFAULT_BRANCH_NAME = 'default'
30 scm = 'hg'
31
32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
33 update_after_clone=False):
34 """
35 Raises RepositoryError if repository could not be find at the given
36 ``repo_path``.
37
38 :param repo_path: local path of the repository
39 :param create=False: if set to True, would try to create repository if
40 it does not exist rather than raising exception
41 :param baseui=None: user data
42 :param src_url=None: would try to clone repository from given location
43 :param update_after_clone=False: sets update of working copy after
44 making a clone
45 """
46
47 if not isinstance(repo_path, str):
48 raise VCSError('Mercurial backend requires repository path to '
49 'be instance of <str> got %s instead' %
50 type(repo_path))
51
52 self.path = abspath(repo_path)
53 self.baseui = baseui or ui.ui()
54 # We've set path and ui, now we can set _repo itself
55 self._repo = self._get_repo(create, src_url, update_after_clone)
56
57 @property
58 def _empty(self):
59 """
60 Checks if repository is empty without any changesets
61 """
62 # TODO: Following raises errors when using InMemoryChangeset...
63 # return len(self._repo.changelog) == 0
64 return len(self.revisions) == 0
65
66 @LazyProperty
67 def revisions(self):
68 """
69 Returns list of revisions' ids, in ascending order. Being lazy
70 attribute allows external tools to inject shas from cache.
71 """
72 return self._get_all_revisions()
73
74 @LazyProperty
75 def name(self):
76 return os.path.basename(self.path)
77
78 @LazyProperty
79 def branches(self):
80 return self._get_branches()
81
82 def _get_branches(self, closed=False):
83 """
84 Get's branches for this repository
85 Returns only not closed branches by default
86
87 :param closed: return also closed branches for mercurial
88 """
89
90 if self._empty:
91 return {}
92
93 def _branchtags(localrepo):
94 """
95 Patched version of mercurial branchtags to not return the closed
96 branches
97
98 :param localrepo: locarepository instance
99 """
100
101 bt = {}
102 bt_closed = {}
103 for bn, heads in localrepo.branchmap().iteritems():
104 tip = heads[-1]
105 if 'close' in localrepo.changelog.read(tip)[5]:
106 bt_closed[bn] = tip
107 else:
108 bt[bn] = tip
109
110 if closed:
111 bt.update(bt_closed)
112 return bt
113
114 sortkey = lambda ctx: ctx[0] # sort by name
115 _branches = [(safe_unicode(n), hex(h),) for n, h in
116 _branchtags(self._repo).items()]
117
118 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
119
120 @LazyProperty
121 def tags(self):
122 """
123 Get's tags for this repository
124 """
125 return self._get_tags()
126
127 def _get_tags(self):
128 if self._empty:
129 return {}
130
131 sortkey = lambda ctx: ctx[0] # sort by name
132 _tags = [(safe_unicode(n), hex(h),) for n, h in
133 self._repo.tags().items()]
134
135 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
136
137 def tag(self, name, user, revision=None, message=None, date=None,
138 **kwargs):
139 """
140 Creates and returns a tag for the given ``revision``.
141
142 :param name: name for new tag
143 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
144 :param revision: changeset id for which new tag would be created
145 :param message: message of the tag's commit
146 :param date: date of tag's commit
147
148 :raises TagAlreadyExistError: if tag with same name already exists
149 """
150 if name in self.tags:
151 raise TagAlreadyExistError("Tag %s already exists" % name)
152 changeset = self.get_changeset(revision)
153 local = kwargs.setdefault('local', False)
154
155 if message is None:
156 message = "Added tag %s for changeset %s" % (name,
157 changeset.short_id)
158
159 if date is None:
160 date = datetime.datetime.now().ctime()
161
162 try:
163 self._repo.tag(name, changeset._ctx.node(), message, local, user,
164 date)
165 except Abort, e:
166 raise RepositoryError(e.message)
167
168 # Reinitialize tags
169 self.tags = self._get_tags()
170 tag_id = self.tags[name]
171
172 return self.get_changeset(revision=tag_id)
173
174 def remove_tag(self, name, user, message=None, date=None):
175 """
176 Removes tag with the given ``name``.
177
178 :param name: name of the tag to be removed
179 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
180 :param message: message of the tag's removal commit
181 :param date: date of tag's removal commit
182
183 :raises TagDoesNotExistError: if tag with given name does not exists
184 """
185 if name not in self.tags:
186 raise TagDoesNotExistError("Tag %s does not exist" % name)
187 if message is None:
188 message = "Removed tag %s" % name
189 if date is None:
190 date = datetime.datetime.now().ctime()
191 local = False
192
193 try:
194 self._repo.tag(name, nullid, message, local, user, date)
195 self.tags = self._get_tags()
196 except Abort, e:
197 raise RepositoryError(e.message)
198
199 @LazyProperty
200 def bookmarks(self):
201 """
202 Get's bookmarks for this repository
203 """
204 return self._get_bookmarks()
205
206 def _get_bookmarks(self):
207 if self._empty:
208 return {}
209
210 sortkey = lambda ctx: ctx[0] # sort by name
211 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
212 self._repo._bookmarks.items()]
213 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
214
215 def _get_all_revisions(self):
216
217 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
218
219 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
220 context=3):
221 """
222 Returns (git like) *diff*, as plain text. Shows changes introduced by
223 ``rev2`` since ``rev1``.
224
225 :param rev1: Entry point from which diff is shown. Can be
226 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
227 the changes since empty state of the repository until ``rev2``
228 :param rev2: Until which revision changes should be shown.
229 :param ignore_whitespace: If set to ``True``, would not show whitespace
230 changes. Defaults to ``False``.
231 :param context: How many lines before/after changed lines should be
232 shown. Defaults to ``3``.
233 """
234 # Check if given revisions are present at repository (may raise
235 # ChangesetDoesNotExistError)
236 if rev1 != self.EMPTY_CHANGESET:
237 self.get_changeset(rev1)
238 self.get_changeset(rev2)
239
240 file_filter = match(self.path, '', [path])
241 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
242 opts=diffopts(git=True,
243 ignorews=ignore_whitespace,
244 context=context)))
245
246 def _check_url(self, url):
247 """
248 Function will check given url and try to verify if it's a valid
249 link. Sometimes it may happened that mercurial will issue basic
250 auth request that can cause whole API to hang when used from python
251 or other external calls.
252
253 On failures it'll raise urllib2.HTTPError, return code 200 if url
254 is valid or True if it's a local path
255 """
256
257 from mercurial.util import url as Url
258
259 # those authnadlers are patched for python 2.6.5 bug an
260 # infinit looping when given invalid resources
261 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
262
263 # check first if it's not an local url
264 if os.path.isdir(url) or url.startswith('file:'):
265 return True
266
267 handlers = []
268 test_uri, authinfo = Url(url).authinfo()
269
270 if authinfo:
271 #create a password manager
272 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
273 passmgr.add_password(*authinfo)
274
275 handlers.extend((httpbasicauthhandler(passmgr),
276 httpdigestauthhandler(passmgr)))
277
278 o = urllib2.build_opener(*handlers)
279 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
280 ('Accept', 'application/mercurial-0.1')]
281
282 q = {"cmd": 'between'}
283 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
284 qs = '?%s' % urllib.urlencode(q)
285 cu = "%s%s" % (test_uri, qs)
286 req = urllib2.Request(cu, None, {})
287
288 try:
289 resp = o.open(req)
290 return resp.code == 200
291 except Exception, e:
292 # means it cannot be cloned
293 raise urllib2.URLError(e)
294
295 def _get_repo(self, create, src_url=None, update_after_clone=False):
296 """
297 Function will check for mercurial repository in given path and return
298 a localrepo object. If there is no repository in that path it will
299 raise an exception unless ``create`` parameter is set to True - in
300 that case repository would be created and returned.
301 If ``src_url`` is given, would try to clone repository from the
302 location at given clone_point. Additionally it'll make update to
303 working copy accordingly to ``update_after_clone`` flag
304 """
305 try:
306 if src_url:
307 url = str(self._get_url(src_url))
308 opts = {}
309 if not update_after_clone:
310 opts.update({'noupdate': True})
311 try:
312 self._check_url(url)
313 clone(self.baseui, url, self.path, **opts)
314 # except urllib2.URLError:
315 # raise Abort("Got HTTP 404 error")
316 except Exception:
317 raise
318 # Don't try to create if we've already cloned repo
319 create = False
320 return localrepository(self.baseui, self.path, create=create)
321 except (Abort, RepoError), err:
322 if create:
323 msg = "Cannot create repository at %s. Original error was %s"\
324 % (self.path, err)
325 else:
326 msg = "Not valid repository at %s. Original error was %s"\
327 % (self.path, err)
328 raise RepositoryError(msg)
329
330 @LazyProperty
331 def in_memory_changeset(self):
332 return MercurialInMemoryChangeset(self)
333
334 @LazyProperty
335 def description(self):
336 undefined_description = u'unknown'
337 return safe_unicode(self._repo.ui.config('web', 'description',
338 undefined_description, untrusted=True))
339
340 @LazyProperty
341 def contact(self):
342 undefined_contact = u'Unknown'
343 return safe_unicode(get_contact(self._repo.ui.config)
344 or undefined_contact)
345
346 @LazyProperty
347 def last_change(self):
348 """
349 Returns last change made on this repository as datetime object
350 """
351 return date_fromtimestamp(self._get_mtime(), makedate()[1])
352
353 def _get_mtime(self):
354 try:
355 return time.mktime(self.get_changeset().date.timetuple())
356 except RepositoryError:
357 #fallback to filesystem
358 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
359 st_path = os.path.join(self.path, '.hg', "store")
360 if os.path.exists(cl_path):
361 return os.stat(cl_path).st_mtime
362 else:
363 return os.stat(st_path).st_mtime
364
365 def _get_hidden(self):
366 return self._repo.ui.configbool("web", "hidden", untrusted=True)
367
368 def _get_revision(self, revision):
369 """
370 Get's an ID revision given as str. This will always return a fill
371 40 char revision number
372
373 :param revision: str or int or None
374 """
375
376 if self._empty:
377 raise EmptyRepositoryError("There are no changesets yet")
378
379 if revision in [-1, 'tip', None]:
380 revision = 'tip'
381
382 try:
383 revision = hex(self._repo.lookup(revision))
384 except (IndexError, ValueError, RepoLookupError, TypeError):
385 raise ChangesetDoesNotExistError("Revision %r does not "
386 "exist for this repository %s" \
387 % (revision, self))
388 return revision
389
390 def _get_archives(self, archive_name='tip'):
391 allowed = self.baseui.configlist("web", "allow_archive",
392 untrusted=True)
393 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
394 if i[0] in allowed or self._repo.ui.configbool("web",
395 "allow" + i[0],
396 untrusted=True):
397 yield {"type": i[0], "extension": i[1], "node": archive_name}
398
399 def _get_url(self, url):
400 """
401 Returns normalized url. If schema is not given, would fall
402 to filesystem
403 (``file:///``) schema.
404 """
405 url = str(url)
406 if url != 'default' and not '://' in url:
407 url = "file:" + urllib.pathname2url(url)
408 return url
409
410 def get_changeset(self, revision=None):
411 """
412 Returns ``MercurialChangeset`` object representing repository's
413 changeset at the given ``revision``.
414 """
415 revision = self._get_revision(revision)
416 changeset = MercurialChangeset(repository=self, revision=revision)
417 return changeset
418
419 def get_changesets(self, start=None, end=None, start_date=None,
420 end_date=None, branch_name=None, reverse=False):
421 """
422 Returns iterator of ``MercurialChangeset`` objects from start to end
423 (both are inclusive)
424
425 :param start: None, str, int or mercurial lookup format
426 :param end: None, str, int or mercurial lookup format
427 :param start_date:
428 :param end_date:
429 :param branch_name:
430 :param reversed: return changesets in reversed order
431 """
432
433 start_raw_id = self._get_revision(start)
434 start_pos = self.revisions.index(start_raw_id) if start else None
435 end_raw_id = self._get_revision(end)
436 end_pos = self.revisions.index(end_raw_id) if end else None
437
438 if None not in [start, end] and start_pos > end_pos:
439 raise RepositoryError("start revision '%s' cannot be "
440 "after end revision '%s'" % (start, end))
441
442 if branch_name and branch_name not in self.branches.keys():
443 raise BranchDoesNotExistError('Such branch %s does not exists for'
444 ' this repository' % branch_name)
445 if end_pos is not None:
446 end_pos += 1
447
448 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
449 self.revisions[start_pos:end_pos]
450
451 for id_ in slice_:
452 cs = self.get_changeset(id_)
453 if branch_name and cs.branch != branch_name:
454 continue
455 if start_date and cs.date < start_date:
456 continue
457 if end_date and cs.date > end_date:
458 continue
459
460 yield cs
461
462 def pull(self, url):
463 """
464 Tries to pull changes from external location.
465 """
466 url = self._get_url(url)
467 try:
468 pull(self.baseui, self._repo, url)
469 except Abort, err:
470 # Propagate error but with vcs's type
471 raise RepositoryError(str(err))
472
473 @LazyProperty
474 def workdir(self):
475 """
476 Returns ``Workdir`` instance for this repository.
477 """
478 return MercurialWorkdir(self)
479
480 def get_config_value(self, section, name, config_file=None):
481 """
482 Returns configuration value for a given [``section``] and ``name``.
483
484 :param section: Section we want to retrieve value from
485 :param name: Name of configuration we want to retrieve
486 :param config_file: A path to file which should be used to retrieve
487 configuration from (might also be a list of file paths)
488 """
489 if config_file is None:
490 config_file = []
491 elif isinstance(config_file, basestring):
492 config_file = [config_file]
493
494 config = self._repo.ui
495 for path in config_file:
496 config.readconfig(path)
497 return config.config(section, name)
498
499 def get_user_name(self, config_file=None):
500 """
501 Returns user's name from global configuration file.
502
503 :param config_file: A path to file which should be used to retrieve
504 configuration from (might also be a list of file paths)
505 """
506 username = self.get_config_value('ui', 'username')
507 if username:
508 return author_name(username)
509 return None
510
511 def get_user_email(self, config_file=None):
512 """
513 Returns user's email from global configuration file.
514
515 :param config_file: A path to file which should be used to retrieve
516 configuration from (might also be a list of file paths)
517 """
518 username = self.get_config_value('ui', 'username')
519 if username:
520 return author_email(username)
521 return None
@@ -0,0 +1,21 b''
1 from rhodecode.lib.vcs.backends.base import BaseWorkdir
2 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
3
4 from ...utils.hgcompat import hg_merge
5
6
7 class MercurialWorkdir(BaseWorkdir):
8
9 def get_branch(self):
10 return self.repository._repo.dirstate.branch()
11
12 def get_changeset(self):
13 return self.repository.get_changeset()
14
15 def checkout_branch(self, branch=None):
16 if branch is None:
17 branch = self.repository.DEFAULT_BRANCH_NAME
18 if branch not in self.repository.branches:
19 raise BranchDoesNotExistError
20
21 hg_merge.update(self.repository._repo, branch, False, False, None)
1 NO CONTENT: new file 100644
@@ -0,0 +1,33 b''
1 import os
2 import tempfile
3 from rhodecode.lib.vcs.utils.paths import get_user_home
4
5 abspath = lambda * p: os.path.abspath(os.path.join(*p))
6
7 VCSRC_PATH = os.environ.get('VCSRC_PATH')
8
9 if not VCSRC_PATH:
10 HOME_ = get_user_home()
11 if not HOME_:
12 HOME_ = tempfile.gettempdir()
13
14 VCSRC_PATH = VCSRC_PATH or abspath(HOME_, '.vcsrc')
15 if os.path.isdir(VCSRC_PATH):
16 VCSRC_PATH = os.path.join(VCSRC_PATH, '__init__.py')
17
18 BACKENDS = {
19 'hg': 'vcs.backends.hg.MercurialRepository',
20 'git': 'vcs.backends.git.GitRepository',
21 }
22
23 ARCHIVE_SPECS = {
24 'tar': ('application/x-tar', '.tar'),
25 'tbz2': ('application/x-bzip2', '.tar.bz2'),
26 'tgz': ('application/x-gzip', '.tar.gz'),
27 'zip': ('application/zip', '.zip'),
28 }
29
30 BACKENDS = {
31 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
32 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
33 }
@@ -0,0 +1,93 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs.exceptions
4 ~~~~~~~~~~~~~~
5
6 Custom exceptions module
7
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
11
12
13 class VCSError(Exception):
14 pass
15
16
17 class RepositoryError(VCSError):
18 pass
19
20
21 class EmptyRepositoryError(RepositoryError):
22 pass
23
24
25 class TagAlreadyExistError(RepositoryError):
26 pass
27
28
29 class TagDoesNotExistError(RepositoryError):
30 pass
31
32
33 class BranchAlreadyExistError(RepositoryError):
34 pass
35
36
37 class BranchDoesNotExistError(RepositoryError):
38 pass
39
40
41 class ChangesetError(RepositoryError):
42 pass
43
44
45 class ChangesetDoesNotExistError(ChangesetError):
46 pass
47
48
49 class CommitError(RepositoryError):
50 pass
51
52
53 class NothingChangedError(CommitError):
54 pass
55
56
57 class NodeError(VCSError):
58 pass
59
60
61 class RemovedFileNodeError(NodeError):
62 pass
63
64
65 class NodeAlreadyExistsError(CommitError):
66 pass
67
68
69 class NodeAlreadyChangedError(CommitError):
70 pass
71
72
73 class NodeDoesNotExistError(CommitError):
74 pass
75
76
77 class NodeNotChangedError(CommitError):
78 pass
79
80
81 class NodeAlreadyAddedError(CommitError):
82 pass
83
84
85 class NodeAlreadyRemovedError(CommitError):
86 pass
87
88
89 class ImproperArchiveTypeError(VCSError):
90 pass
91
92 class CommandError(VCSError):
93 pass
This diff has been collapsed as it changes many lines, (551 lines changed) Show them Hide them
@@ -0,0 +1,551 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs.nodes
4 ~~~~~~~~~
5
6 Module holding everything related to vcs nodes.
7
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
11 import stat
12 import posixpath
13 import mimetypes
14
15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 from rhodecode.lib.vcs.utils import safe_unicode
17 from rhodecode.lib.vcs.exceptions import NodeError
18 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
19
20 from pygments import lexers
21
22
23 class NodeKind:
24 DIR = 1
25 FILE = 2
26
27
28 class NodeState:
29 ADDED = u'added'
30 CHANGED = u'changed'
31 NOT_CHANGED = u'not changed'
32 REMOVED = u'removed'
33
34
35 class NodeGeneratorBase(object):
36 """
37 Base class for removed added and changed filenodes, it's a lazy generator
38 class that will create filenodes only on iteration or call
39
40 The len method doesn't need to create filenodes at all
41 """
42
43 def __init__(self, current_paths, cs):
44 self.cs = cs
45 self.current_paths = current_paths
46
47 def __call__(self):
48 return [n for n in self]
49
50 def __getslice__(self, i, j):
51 for p in self.current_paths[i:j]:
52 yield self.cs.get_node(p)
53
54 def __len__(self):
55 return len(self.current_paths)
56
57 def __iter__(self):
58 for p in self.current_paths:
59 yield self.cs.get_node(p)
60
61
62 class AddedFileNodesGenerator(NodeGeneratorBase):
63 """
64 Class holding Added files for current changeset
65 """
66 pass
67
68
69 class ChangedFileNodesGenerator(NodeGeneratorBase):
70 """
71 Class holding Changed files for current changeset
72 """
73 pass
74
75
76 class RemovedFileNodesGenerator(NodeGeneratorBase):
77 """
78 Class holding removed files for current changeset
79 """
80 def __iter__(self):
81 for p in self.current_paths:
82 yield RemovedFileNode(path=p)
83
84 def __getslice__(self, i, j):
85 for p in self.current_paths[i:j]:
86 yield RemovedFileNode(path=p)
87
88
89 class Node(object):
90 """
91 Simplest class representing file or directory on repository. SCM backends
92 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
93 directly.
94
95 Node's ``path`` cannot start with slash as we operate on *relative* paths
96 only. Moreover, every single node is identified by the ``path`` attribute,
97 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
98 """
99
100 def __init__(self, path, kind):
101 if path.startswith('/'):
102 raise NodeError("Cannot initialize Node objects with slash at "
103 "the beginning as only relative paths are supported")
104 self.path = path.rstrip('/')
105 if path == '' and kind != NodeKind.DIR:
106 raise NodeError("Only DirNode and its subclasses may be "
107 "initialized with empty path")
108 self.kind = kind
109 #self.dirs, self.files = [], []
110 if self.is_root() and not self.is_dir():
111 raise NodeError("Root node cannot be FILE kind")
112
113 @LazyProperty
114 def parent(self):
115 parent_path = self.get_parent_path()
116 if parent_path:
117 if self.changeset:
118 return self.changeset.get_node(parent_path)
119 return DirNode(parent_path)
120 return None
121
122 @LazyProperty
123 def name(self):
124 """
125 Returns name of the node so if its path
126 then only last part is returned.
127 """
128 return safe_unicode(self.path.rstrip('/').split('/')[-1])
129
130 def _get_kind(self):
131 return self._kind
132
133 def _set_kind(self, kind):
134 if hasattr(self, '_kind'):
135 raise NodeError("Cannot change node's kind")
136 else:
137 self._kind = kind
138 # Post setter check (path's trailing slash)
139 if self.path.endswith('/'):
140 raise NodeError("Node's path cannot end with slash")
141
142 kind = property(_get_kind, _set_kind)
143
144 def __cmp__(self, other):
145 """
146 Comparator using name of the node, needed for quick list sorting.
147 """
148 kind_cmp = cmp(self.kind, other.kind)
149 if kind_cmp:
150 return kind_cmp
151 return cmp(self.name, other.name)
152
153 def __eq__(self, other):
154 for attr in ['name', 'path', 'kind']:
155 if getattr(self, attr) != getattr(other, attr):
156 return False
157 if self.is_file():
158 if self.content != other.content:
159 return False
160 else:
161 # For DirNode's check without entering each dir
162 self_nodes_paths = list(sorted(n.path for n in self.nodes))
163 other_nodes_paths = list(sorted(n.path for n in self.nodes))
164 if self_nodes_paths != other_nodes_paths:
165 return False
166 return True
167
168 def __nq__(self, other):
169 return not self.__eq__(other)
170
171 def __repr__(self):
172 return '<%s %r>' % (self.__class__.__name__, self.path)
173
174 def __str__(self):
175 return self.__repr__()
176
177 def __unicode__(self):
178 return self.name
179
180 def get_parent_path(self):
181 """
182 Returns node's parent path or empty string if node is root.
183 """
184 if self.is_root():
185 return ''
186 return posixpath.dirname(self.path.rstrip('/')) + '/'
187
188 def is_file(self):
189 """
190 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
191 otherwise.
192 """
193 return self.kind == NodeKind.FILE
194
195 def is_dir(self):
196 """
197 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
198 otherwise.
199 """
200 return self.kind == NodeKind.DIR
201
202 def is_root(self):
203 """
204 Returns ``True`` if node is a root node and ``False`` otherwise.
205 """
206 return self.kind == NodeKind.DIR and self.path == ''
207
208 @LazyProperty
209 def added(self):
210 return self.state is NodeState.ADDED
211
212 @LazyProperty
213 def changed(self):
214 return self.state is NodeState.CHANGED
215
216 @LazyProperty
217 def not_changed(self):
218 return self.state is NodeState.NOT_CHANGED
219
220 @LazyProperty
221 def removed(self):
222 return self.state is NodeState.REMOVED
223
224
225 class FileNode(Node):
226 """
227 Class representing file nodes.
228
229 :attribute: path: path to the node, relative to repostiory's root
230 :attribute: content: if given arbitrary sets content of the file
231 :attribute: changeset: if given, first time content is accessed, callback
232 :attribute: mode: octal stat mode for a node. Default is 0100644.
233 """
234
235 def __init__(self, path, content=None, changeset=None, mode=None):
236 """
237 Only one of ``content`` and ``changeset`` may be given. Passing both
238 would raise ``NodeError`` exception.
239
240 :param path: relative path to the node
241 :param content: content may be passed to constructor
242 :param changeset: if given, will use it to lazily fetch content
243 :param mode: octal representation of ST_MODE (i.e. 0100644)
244 """
245
246 if content and changeset:
247 raise NodeError("Cannot use both content and changeset")
248 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
249 self.changeset = changeset
250 self._content = content
251 self._mode = mode or 0100644
252
253 @LazyProperty
254 def mode(self):
255 """
256 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
257 use value given at initialization or 0100644 (default).
258 """
259 if self.changeset:
260 mode = self.changeset.get_file_mode(self.path)
261 else:
262 mode = self._mode
263 return mode
264
265 @property
266 def content(self):
267 """
268 Returns lazily content of the FileNode. If possible, would try to
269 decode content from UTF-8.
270 """
271 if self.changeset:
272 content = self.changeset.get_file_content(self.path)
273 else:
274 content = self._content
275
276 if bool(content and '\0' in content):
277 return content
278 return safe_unicode(content)
279
280 @LazyProperty
281 def size(self):
282 if self.changeset:
283 return self.changeset.get_file_size(self.path)
284 raise NodeError("Cannot retrieve size of the file without related "
285 "changeset attribute")
286
287 @LazyProperty
288 def message(self):
289 if self.changeset:
290 return self.last_changeset.message
291 raise NodeError("Cannot retrieve message of the file without related "
292 "changeset attribute")
293
294 @LazyProperty
295 def last_changeset(self):
296 if self.changeset:
297 return self.changeset.get_file_changeset(self.path)
298 raise NodeError("Cannot retrieve last changeset of the file without "
299 "related changeset attribute")
300
301 def get_mimetype(self):
302 """
303 Mimetype is calculated based on the file's content. If ``_mimetype``
304 attribute is available, it will be returned (backends which store
305 mimetypes or can easily recognize them, should set this private
306 attribute to indicate that type should *NOT* be calculated).
307 """
308 if hasattr(self, '_mimetype'):
309 if (isinstance(self._mimetype,(tuple,list,)) and
310 len(self._mimetype) == 2):
311 return self._mimetype
312 else:
313 raise NodeError('given _mimetype attribute must be an 2 '
314 'element list or tuple')
315
316 mtype,encoding = mimetypes.guess_type(self.name)
317
318 if mtype is None:
319 if self.is_binary:
320 mtype = 'application/octet-stream'
321 encoding = None
322 else:
323 mtype = 'text/plain'
324 encoding = None
325 return mtype,encoding
326
327 @LazyProperty
328 def mimetype(self):
329 """
330 Wrapper around full mimetype info. It returns only type of fetched
331 mimetype without the encoding part. use get_mimetype function to fetch
332 full set of (type,encoding)
333 """
334 return self.get_mimetype()[0]
335
336 @LazyProperty
337 def mimetype_main(self):
338 return self.mimetype.split('/')[0]
339
340 @LazyProperty
341 def lexer(self):
342 """
343 Returns pygment's lexer class. Would try to guess lexer taking file's
344 content, name and mimetype.
345 """
346 try:
347 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
348 except lexers.ClassNotFound:
349 lexer = lexers.TextLexer()
350 # returns first alias
351 return lexer
352
353 @LazyProperty
354 def lexer_alias(self):
355 """
356 Returns first alias of the lexer guessed for this file.
357 """
358 return self.lexer.aliases[0]
359
360 @LazyProperty
361 def history(self):
362 """
363 Returns a list of changeset for this file in which the file was changed
364 """
365 if self.changeset is None:
366 raise NodeError('Unable to get changeset for this FileNode')
367 return self.changeset.get_file_history(self.path)
368
369 @LazyProperty
370 def annotate(self):
371 """
372 Returns a list of three element tuples with lineno,changeset and line
373 """
374 if self.changeset is None:
375 raise NodeError('Unable to get changeset for this FileNode')
376 return self.changeset.get_file_annotate(self.path)
377
378 @LazyProperty
379 def state(self):
380 if not self.changeset:
381 raise NodeError("Cannot check state of the node if it's not "
382 "linked with changeset")
383 elif self.path in (node.path for node in self.changeset.added):
384 return NodeState.ADDED
385 elif self.path in (node.path for node in self.changeset.changed):
386 return NodeState.CHANGED
387 else:
388 return NodeState.NOT_CHANGED
389
390 @property
391 def is_binary(self):
392 """
393 Returns True if file has binary content.
394 """
395 bin = '\0' in self.content
396 return bin
397
398 @LazyProperty
399 def extension(self):
400 """Returns filenode extension"""
401 return self.name.split('.')[-1]
402
403 def is_executable(self):
404 """
405 Returns ``True`` if file has executable flag turned on.
406 """
407 return bool(self.mode & stat.S_IXUSR)
408
409
410 class RemovedFileNode(FileNode):
411 """
412 Dummy FileNode class - trying to access any public attribute except path,
413 name, kind or state (or methods/attributes checking those two) would raise
414 RemovedFileNodeError.
415 """
416 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
417 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
418
419 def __init__(self, path):
420 """
421 :param path: relative path to the node
422 """
423 super(RemovedFileNode, self).__init__(path=path)
424
425 def __getattribute__(self, attr):
426 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
427 return super(RemovedFileNode, self).__getattribute__(attr)
428 raise RemovedFileNodeError("Cannot access attribute %s on "
429 "RemovedFileNode" % attr)
430
431 @LazyProperty
432 def state(self):
433 return NodeState.REMOVED
434
435
436 class DirNode(Node):
437 """
438 DirNode stores list of files and directories within this node.
439 Nodes may be used standalone but within repository context they
440 lazily fetch data within same repositorty's changeset.
441 """
442
443 def __init__(self, path, nodes=(), changeset=None):
444 """
445 Only one of ``nodes`` and ``changeset`` may be given. Passing both
446 would raise ``NodeError`` exception.
447
448 :param path: relative path to the node
449 :param nodes: content may be passed to constructor
450 :param changeset: if given, will use it to lazily fetch content
451 :param size: always 0 for ``DirNode``
452 """
453 if nodes and changeset:
454 raise NodeError("Cannot use both nodes and changeset")
455 super(DirNode, self).__init__(path, NodeKind.DIR)
456 self.changeset = changeset
457 self._nodes = nodes
458
459 @LazyProperty
460 def content(self):
461 raise NodeError("%s represents a dir and has no ``content`` attribute"
462 % self)
463
464 @LazyProperty
465 def nodes(self):
466 if self.changeset:
467 nodes = self.changeset.get_nodes(self.path)
468 else:
469 nodes = self._nodes
470 self._nodes_dict = dict((node.path, node) for node in nodes)
471 return sorted(nodes)
472
473 @LazyProperty
474 def files(self):
475 return sorted((node for node in self.nodes if node.is_file()))
476
477 @LazyProperty
478 def dirs(self):
479 return sorted((node for node in self.nodes if node.is_dir()))
480
481 def __iter__(self):
482 for node in self.nodes:
483 yield node
484
485 def get_node(self, path):
486 """
487 Returns node from within this particular ``DirNode``, so it is now
488 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
489 'docs'. In order to access deeper nodes one must fetch nodes between
490 them first - this would work::
491
492 docs = root.get_node('docs')
493 docs.get_node('api').get_node('index.rst')
494
495 :param: path - relative to the current node
496
497 .. note::
498 To access lazily (as in example above) node have to be initialized
499 with related changeset object - without it node is out of
500 context and may know nothing about anything else than nearest
501 (located at same level) nodes.
502 """
503 try:
504 path = path.rstrip('/')
505 if path == '':
506 raise NodeError("Cannot retrieve node without path")
507 self.nodes # access nodes first in order to set _nodes_dict
508 paths = path.split('/')
509 if len(paths) == 1:
510 if not self.is_root():
511 path = '/'.join((self.path, paths[0]))
512 else:
513 path = paths[0]
514 return self._nodes_dict[path]
515 elif len(paths) > 1:
516 if self.changeset is None:
517 raise NodeError("Cannot access deeper "
518 "nodes without changeset")
519 else:
520 path1, path2 = paths[0], '/'.join(paths[1:])
521 return self.get_node(path1).get_node(path2)
522 else:
523 raise KeyError
524 except KeyError:
525 raise NodeError("Node does not exist at %s" % path)
526
527 @LazyProperty
528 def state(self):
529 raise NodeError("Cannot access state of DirNode")
530
531 @LazyProperty
532 def size(self):
533 size = 0
534 for root, dirs, files in self.changeset.walk(self.path):
535 for f in files:
536 size += f.size
537
538 return size
539
540
541 class RootNode(DirNode):
542 """
543 DirNode being the root node of the repository.
544 """
545
546 def __init__(self, nodes=(), changeset=None):
547 super(RootNode, self).__init__(path='', nodes=nodes,
548 changeset=changeset)
549
550 def __repr__(self):
551 return '<%s>' % self.__class__.__name__
@@ -0,0 +1,133 b''
1 """
2 This module provides some useful tools for ``vcs`` like annotate/diff html
3 output. It also includes some internal helpers.
4 """
5 import sys
6 import time
7 import datetime
8
9
10 def makedate():
11 lt = time.localtime()
12 if lt[8] == 1 and time.daylight:
13 tz = time.altzone
14 else:
15 tz = time.timezone
16 return time.mktime(lt), tz
17
18
19 def date_fromtimestamp(unixts, tzoffset=0):
20 """
21 Makes a local datetime object out of unix timestamp
22
23 :param unixts:
24 :param tzoffset:
25 """
26
27 return datetime.datetime.fromtimestamp(float(unixts))
28
29
30 def safe_unicode(str_, from_encoding='utf8'):
31 """
32 safe unicode function. Does few trick to turn str_ into unicode
33
34 In case of UnicodeDecode error we try to return it with encoding detected
35 by chardet library if it fails fallback to unicode with errors replaced
36
37 :param str_: string to decode
38 :rtype: unicode
39 :returns: unicode object
40 """
41 if isinstance(str_, unicode):
42 return str_
43
44 try:
45 return unicode(str_)
46 except UnicodeDecodeError:
47 pass
48
49 try:
50 return unicode(str_, from_encoding)
51 except UnicodeDecodeError:
52 pass
53
54 try:
55 import chardet
56 encoding = chardet.detect(str_)['encoding']
57 if encoding is None:
58 raise Exception()
59 return str_.decode(encoding)
60 except (ImportError, UnicodeDecodeError, Exception):
61 return unicode(str_, from_encoding, 'replace')
62
63
64 def safe_str(unicode_, to_encoding='utf8'):
65 """
66 safe str function. Does few trick to turn unicode_ into string
67
68 In case of UnicodeEncodeError we try to return it with encoding detected
69 by chardet library if it fails fallback to string with errors replaced
70
71 :param unicode_: unicode to encode
72 :rtype: str
73 :returns: str object
74 """
75
76 if isinstance(unicode_, str):
77 return unicode_
78
79 try:
80 return unicode_.encode(to_encoding)
81 except UnicodeEncodeError:
82 pass
83
84 try:
85 import chardet
86 encoding = chardet.detect(unicode_)['encoding']
87 print encoding
88 if encoding is None:
89 raise UnicodeEncodeError()
90
91 return unicode_.encode(encoding)
92 except (ImportError, UnicodeEncodeError):
93 return unicode_.encode(to_encoding, 'replace')
94
95 return safe_str
96
97
98 def author_email(author):
99 """
100 returns email address of given author.
101 If any of <,> sign are found, it fallbacks to regex findall()
102 and returns first found result or empty string
103
104 Regex taken from http://www.regular-expressions.info/email.html
105 """
106 import re
107 r = author.find('>')
108 l = author.find('<')
109
110 if l == -1 or r == -1:
111 # fallback to regex match of email out of a string
112 email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!"""
113 r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z"""
114 r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]"""
115 r"""*[a-z0-9])?""", re.IGNORECASE)
116 m = re.findall(email_re, author)
117 return m[0] if m else ''
118
119 return author[l + 1:r].strip()
120
121
122 def author_name(author):
123 """
124 get name of author, or else username.
125 It'll try to find an email in the author string and just cut it off
126 to get the username
127 """
128
129 if not '@' in author:
130 return author
131 else:
132 return author.replace(author_email(author), '').replace('<', '')\
133 .replace('>', '').strip()
@@ -0,0 +1,177 b''
1 from rhodecode.lib.vcs.exceptions import VCSError
2 from rhodecode.lib.vcs.nodes import FileNode
3 from pygments.formatters import HtmlFormatter
4 from pygments import highlight
5
6 import StringIO
7
8
9 def annotate_highlight(filenode, annotate_from_changeset_func=None,
10 order=None, headers=None, **options):
11 """
12 Returns html portion containing annotated table with 3 columns: line
13 numbers, changeset information and pygmentized line of code.
14
15 :param filenode: FileNode object
16 :param annotate_from_changeset_func: function taking changeset and
17 returning single annotate cell; needs break line at the end
18 :param order: ordered sequence of ``ls`` (line numbers column),
19 ``annotate`` (annotate column), ``code`` (code column); Default is
20 ``['ls', 'annotate', 'code']``
21 :param headers: dictionary with headers (keys are whats in ``order``
22 parameter)
23 """
24 options['linenos'] = True
25 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
26 headers=headers,
27 annotate_from_changeset_func=annotate_from_changeset_func, **options)
28 lexer = filenode.lexer
29 highlighted = highlight(filenode.content, lexer, formatter)
30 return highlighted
31
32
33 class AnnotateHtmlFormatter(HtmlFormatter):
34
35 def __init__(self, filenode, annotate_from_changeset_func=None,
36 order=None, **options):
37 """
38 If ``annotate_from_changeset_func`` is passed it should be a function
39 which returns string from the given changeset. For example, we may pass
40 following function as ``annotate_from_changeset_func``::
41
42 def changeset_to_anchor(changeset):
43 return '<a href="/changesets/%s/">%s</a>\n' %\
44 (changeset.id, changeset.id)
45
46 :param annotate_from_changeset_func: see above
47 :param order: (default: ``['ls', 'annotate', 'code']``); order of
48 columns;
49 :param options: standard pygment's HtmlFormatter options, there is
50 extra option tough, ``headers``. For instance we can pass::
51
52 formatter = AnnotateHtmlFormatter(filenode, headers={
53 'ls': '#',
54 'annotate': 'Annotate',
55 'code': 'Code',
56 })
57
58 """
59 super(AnnotateHtmlFormatter, self).__init__(**options)
60 self.annotate_from_changeset_func = annotate_from_changeset_func
61 self.order = order or ('ls', 'annotate', 'code')
62 headers = options.pop('headers', None)
63 if headers and not ('ls' in headers and 'annotate' in headers and
64 'code' in headers):
65 raise ValueError("If headers option dict is specified it must "
66 "all 'ls', 'annotate' and 'code' keys")
67 self.headers = headers
68 if isinstance(filenode, FileNode):
69 self.filenode = filenode
70 else:
71 raise VCSError("This formatter expect FileNode parameter, not %r"
72 % type(filenode))
73
74 def annotate_from_changeset(self, changeset):
75 """
76 Returns full html line for single changeset per annotated line.
77 """
78 if self.annotate_from_changeset_func:
79 return self.annotate_from_changeset_func(changeset)
80 else:
81 return ''.join((changeset.id, '\n'))
82
83 def _wrap_tablelinenos(self, inner):
84 dummyoutfile = StringIO.StringIO()
85 lncount = 0
86 for t, line in inner:
87 if t:
88 lncount += 1
89 dummyoutfile.write(line)
90
91 fl = self.linenostart
92 mw = len(str(lncount + fl - 1))
93 sp = self.linenospecial
94 st = self.linenostep
95 la = self.lineanchors
96 aln = self.anchorlinenos
97 if sp:
98 lines = []
99
100 for i in range(fl, fl + lncount):
101 if i % st == 0:
102 if i % sp == 0:
103 if aln:
104 lines.append('<a href="#%s-%d" class="special">'
105 '%*d</a>' %
106 (la, i, mw, i))
107 else:
108 lines.append('<span class="special">'
109 '%*d</span>' % (mw, i))
110 else:
111 if aln:
112 lines.append('<a href="#%s-%d">'
113 '%*d</a>' % (la, i, mw, i))
114 else:
115 lines.append('%*d' % (mw, i))
116 else:
117 lines.append('')
118 ls = '\n'.join(lines)
119 else:
120 lines = []
121 for i in range(fl, fl + lncount):
122 if i % st == 0:
123 if aln:
124 lines.append('<a href="#%s-%d">%*d</a>' \
125 % (la, i, mw, i))
126 else:
127 lines.append('%*d' % (mw, i))
128 else:
129 lines.append('')
130 ls = '\n'.join(lines)
131
132 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
133 # If pygments cropped last lines break we need do that too
134 ln_cs = len(annotate_changesets)
135 ln_ = len(ls.splitlines())
136 if ln_cs > ln_:
137 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
138 annotate = ''.join((self.annotate_from_changeset(changeset)
139 for changeset in annotate_changesets))
140 # in case you wonder about the seemingly redundant <div> here:
141 # since the content in the other cell also is wrapped in a div,
142 # some browsers in some configurations seem to mess up the formatting.
143 '''
144 yield 0, ('<table class="%stable">' % self.cssclass +
145 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
146 ls + '</pre></div></td>' +
147 '<td class="code">')
148 yield 0, dummyoutfile.getvalue()
149 yield 0, '</td></tr></table>'
150
151 '''
152 headers_row = []
153 if self.headers:
154 headers_row = ['<tr class="annotate-header">']
155 for key in self.order:
156 td = ''.join(('<td>', self.headers[key], '</td>'))
157 headers_row.append(td)
158 headers_row.append('</tr>')
159
160 body_row_start = ['<tr>']
161 for key in self.order:
162 if key == 'ls':
163 body_row_start.append(
164 '<td class="linenos"><div class="linenodiv"><pre>' +
165 ls + '</pre></div></td>')
166 elif key == 'annotate':
167 body_row_start.append(
168 '<td class="annotate"><div class="annotatediv"><pre>' +
169 annotate + '</pre></div></td>')
170 elif key == 'code':
171 body_row_start.append('<td class="code">')
172 yield 0, ('<table class="%stable">' % self.cssclass +
173 ''.join(headers_row) +
174 ''.join(body_row_start)
175 )
176 yield 0, dummyoutfile.getvalue()
177 yield 0, '</td></tr></table>'
@@ -0,0 +1,67 b''
1 # -*- coding: utf-8 -*-
2 """
3 vcs.utils.archivers
4 ~~~~~~~~~~~~~~~~~~~
5
6 set of archiver functions for creating archives from repository content
7
8 :created_on: Jan 21, 2011
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
11
12
13 class BaseArchiver(object):
14
15 def __init__(self):
16 self.archive_file = self._get_archive_file()
17
18 def addfile(self):
19 """
20 Adds a file to archive container
21 """
22 pass
23
24 def close(self):
25 """
26 Closes and finalizes operation of archive container object
27 """
28 self.archive_file.close()
29
30 def _get_archive_file(self):
31 """
32 Returns container for specific archive
33 """
34 raise NotImplementedError()
35
36
37 class TarArchiver(BaseArchiver):
38 pass
39
40
41 class Tbz2Archiver(BaseArchiver):
42 pass
43
44
45 class TgzArchiver(BaseArchiver):
46 pass
47
48
49 class ZipArchiver(BaseArchiver):
50 pass
51
52
53 def get_archiver(self, kind):
54 """
55 Returns instance of archiver class specific to given kind
56
57 :param kind: archive kind
58 """
59
60 archivers = {
61 'tar': TarArchiver,
62 'tbz2': Tbz2Archiver,
63 'tgz': TgzArchiver,
64 'zip': ZipArchiver,
65 }
66
67 return archivers[kind]()
@@ -0,0 +1,47 b''
1 from mercurial import ui, config
2
3
4 def make_ui(self, path='hgwebdir.config'):
5 """
6 A funcion that will read python rc files and make an ui from read options
7
8 :param path: path to mercurial config file
9 """
10 #propagated from mercurial documentation
11 sections = [
12 'alias',
13 'auth',
14 'decode/encode',
15 'defaults',
16 'diff',
17 'email',
18 'extensions',
19 'format',
20 'merge-patterns',
21 'merge-tools',
22 'hooks',
23 'http_proxy',
24 'smtp',
25 'patch',
26 'paths',
27 'profiling',
28 'server',
29 'trusted',
30 'ui',
31 'web',
32 ]
33
34 repos = path
35 baseui = ui.ui()
36 cfg = config.config()
37 cfg.read(repos)
38 self.paths = cfg.items('paths')
39 self.base_path = self.paths[0][1].replace('*', '')
40 self.check_repo_dir(self.paths)
41 self.set_statics(cfg)
42
43 for section in sections:
44 for k, v in cfg.items(section):
45 baseui.setconfig(section, k, v)
46
47 return baseui
@@ -0,0 +1,13 b''
1 """
2 Various utilities to work with Python < 2.7.
3
4 Those utilities may be deleted once ``vcs`` stops support for older Python
5 versions.
6 """
7 import sys
8
9
10 if sys.version_info >= (2, 7):
11 unittest = __import__('unittest')
12 else:
13 unittest = __import__('unittest2')
@@ -0,0 +1,460 b''
1 # -*- coding: utf-8 -*-
2 # original copyright: 2007-2008 by Armin Ronacher
3 # licensed under the BSD license.
4
5 import re
6 import difflib
7 import logging
8
9 from difflib import unified_diff
10 from itertools import tee, imap
11
12 from mercurial.match import match
13
14 from rhodecode.lib.vcs.exceptions import VCSError
15 from rhodecode.lib.vcs.nodes import FileNode, NodeError
16
17
18 def get_udiff(filenode_old, filenode_new,show_whitespace=True):
19 """
20 Returns unified diff between given ``filenode_old`` and ``filenode_new``.
21 """
22 try:
23 filenode_old_date = filenode_old.last_changeset.date
24 except NodeError:
25 filenode_old_date = None
26
27 try:
28 filenode_new_date = filenode_new.last_changeset.date
29 except NodeError:
30 filenode_new_date = None
31
32 for filenode in (filenode_old, filenode_new):
33 if not isinstance(filenode, FileNode):
34 raise VCSError("Given object should be FileNode object, not %s"
35 % filenode.__class__)
36
37 if filenode_old_date and filenode_new_date:
38 if not filenode_old_date < filenode_new_date:
39 logging.debug("Generating udiff for filenodes with not increasing "
40 "dates")
41
42 vcs_udiff = unified_diff(filenode_old.content.splitlines(True),
43 filenode_new.content.splitlines(True),
44 filenode_old.name,
45 filenode_new.name,
46 filenode_old_date,
47 filenode_old_date)
48 return vcs_udiff
49
50
51 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True):
52 """
53 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
54
55 :param ignore_whitespace: ignore whitespaces in diff
56 """
57
58 for filenode in (filenode_old, filenode_new):
59 if not isinstance(filenode, FileNode):
60 raise VCSError("Given object should be FileNode object, not %s"
61 % filenode.__class__)
62
63 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
64 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
65
66 repo = filenode_new.changeset.repository
67 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
68 ignore_whitespace)
69
70 return vcs_gitdiff
71
72
73 class DiffProcessor(object):
74 """
75 Give it a unified diff and it returns a list of the files that were
76 mentioned in the diff together with a dict of meta information that
77 can be used to render it in a HTML template.
78 """
79 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
80
81 def __init__(self, diff, differ='diff', format='udiff'):
82 """
83 :param diff: a text in diff format or generator
84 :param format: format of diff passed, `udiff` or `gitdiff`
85 """
86 if isinstance(diff, basestring):
87 diff = [diff]
88
89 self.__udiff = diff
90 self.__format = format
91 self.adds = 0
92 self.removes = 0
93
94 if isinstance(self.__udiff, basestring):
95 self.lines = iter(self.__udiff.splitlines(1))
96
97 elif self.__format == 'gitdiff':
98 udiff_copy = self.copy_iterator()
99 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
100 else:
101 udiff_copy = self.copy_iterator()
102 self.lines = imap(self.escaper, udiff_copy)
103
104 # Select a differ.
105 if differ == 'difflib':
106 self.differ = self._highlight_line_difflib
107 else:
108 self.differ = self._highlight_line_udiff
109
110 def escaper(self, string):
111 return string.replace('<', '&lt;').replace('>', '&gt;')
112
113 def copy_iterator(self):
114 """
115 make a fresh copy of generator, we should not iterate thru
116 an original as it's needed for repeating operations on
117 this instance of DiffProcessor
118 """
119 self.__udiff, iterator_copy = tee(self.__udiff)
120 return iterator_copy
121
122 def _extract_rev(self, line1, line2):
123 """
124 Extract the filename and revision hint from a line.
125 """
126
127 try:
128 if line1.startswith('--- ') and line2.startswith('+++ '):
129 l1 = line1[4:].split(None, 1)
130 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
131 old_rev = l1[1] if len(l1) == 2 else 'old'
132
133 l2 = line2[4:].split(None, 1)
134 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
135 new_rev = l2[1] if len(l2) == 2 else 'new'
136
137 filename = old_filename if (old_filename !=
138 'dev/null') else new_filename
139
140 return filename, new_rev, old_rev
141 except (ValueError, IndexError):
142 pass
143
144 return None, None, None
145
146 def _parse_gitdiff(self, diffiterator):
147 def line_decoder(l):
148 if l.startswith('+') and not l.startswith('+++'):
149 self.adds += 1
150 elif l.startswith('-') and not l.startswith('---'):
151 self.removes += 1
152 return l.decode('utf8', 'replace')
153
154 output = list(diffiterator)
155 size = len(output)
156
157 if size == 2:
158 l = []
159 l.extend([output[0]])
160 l.extend(output[1].splitlines(1))
161 return map(line_decoder, l)
162 elif size == 1:
163 return map(line_decoder, output[0].splitlines(1))
164 elif size == 0:
165 return []
166
167 raise Exception('wrong size of diff %s' % size)
168
169 def _highlight_line_difflib(self, line, next):
170 """
171 Highlight inline changes in both lines.
172 """
173
174 if line['action'] == 'del':
175 old, new = line, next
176 else:
177 old, new = next, line
178
179 oldwords = re.split(r'(\W)', old['line'])
180 newwords = re.split(r'(\W)', new['line'])
181
182 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
183
184 oldfragments, newfragments = [], []
185 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
186 oldfrag = ''.join(oldwords[i1:i2])
187 newfrag = ''.join(newwords[j1:j2])
188 if tag != 'equal':
189 if oldfrag:
190 oldfrag = '<del>%s</del>' % oldfrag
191 if newfrag:
192 newfrag = '<ins>%s</ins>' % newfrag
193 oldfragments.append(oldfrag)
194 newfragments.append(newfrag)
195
196 old['line'] = "".join(oldfragments)
197 new['line'] = "".join(newfragments)
198
199 def _highlight_line_udiff(self, line, next):
200 """
201 Highlight inline changes in both lines.
202 """
203 start = 0
204 limit = min(len(line['line']), len(next['line']))
205 while start < limit and line['line'][start] == next['line'][start]:
206 start += 1
207 end = -1
208 limit -= start
209 while -end <= limit and line['line'][end] == next['line'][end]:
210 end -= 1
211 end += 1
212 if start or end:
213 def do(l):
214 last = end + len(l['line'])
215 if l['action'] == 'add':
216 tag = 'ins'
217 else:
218 tag = 'del'
219 l['line'] = '%s<%s>%s</%s>%s' % (
220 l['line'][:start],
221 tag,
222 l['line'][start:last],
223 tag,
224 l['line'][last:]
225 )
226 do(line)
227 do(next)
228
229 def _parse_udiff(self):
230 """
231 Parse the diff an return data for the template.
232 """
233 lineiter = self.lines
234 files = []
235 try:
236 line = lineiter.next()
237 # skip first context
238 skipfirst = True
239 while 1:
240 # continue until we found the old file
241 if not line.startswith('--- '):
242 line = lineiter.next()
243 continue
244
245 chunks = []
246 filename, old_rev, new_rev = \
247 self._extract_rev(line, lineiter.next())
248 files.append({
249 'filename': filename,
250 'old_revision': old_rev,
251 'new_revision': new_rev,
252 'chunks': chunks
253 })
254
255 line = lineiter.next()
256 while line:
257 match = self._chunk_re.match(line)
258 if not match:
259 break
260
261 lines = []
262 chunks.append(lines)
263
264 old_line, old_end, new_line, new_end = \
265 [int(x or 1) for x in match.groups()[:-1]]
266 old_line -= 1
267 new_line -= 1
268 context = len(match.groups()) == 5
269 old_end += old_line
270 new_end += new_line
271
272 if context:
273 if not skipfirst:
274 lines.append({
275 'old_lineno': '...',
276 'new_lineno': '...',
277 'action': 'context',
278 'line': line,
279 })
280 else:
281 skipfirst = False
282
283 line = lineiter.next()
284 while old_line < old_end or new_line < new_end:
285 if line:
286 command, line = line[0], line[1:]
287 else:
288 command = ' '
289 affects_old = affects_new = False
290
291 # ignore those if we don't expect them
292 if command in '#@':
293 continue
294 elif command == '+':
295 affects_new = True
296 action = 'add'
297 elif command == '-':
298 affects_old = True
299 action = 'del'
300 else:
301 affects_old = affects_new = True
302 action = 'unmod'
303
304 old_line += affects_old
305 new_line += affects_new
306 lines.append({
307 'old_lineno': affects_old and old_line or '',
308 'new_lineno': affects_new and new_line or '',
309 'action': action,
310 'line': line
311 })
312 line = lineiter.next()
313
314 except StopIteration:
315 pass
316
317 # highlight inline changes
318 for file in files:
319 for chunk in chunks:
320 lineiter = iter(chunk)
321 #first = True
322 try:
323 while 1:
324 line = lineiter.next()
325 if line['action'] != 'unmod':
326 nextline = lineiter.next()
327 if nextline['action'] == 'unmod' or \
328 nextline['action'] == line['action']:
329 continue
330 self.differ(line, nextline)
331 except StopIteration:
332 pass
333
334 return files
335
336 def prepare(self):
337 """
338 Prepare the passed udiff for HTML rendering. It'l return a list
339 of dicts
340 """
341 return self._parse_udiff()
342
343 def _safe_id(self, idstring):
344 """Make a string safe for including in an id attribute.
345
346 The HTML spec says that id attributes 'must begin with
347 a letter ([A-Za-z]) and may be followed by any number
348 of letters, digits ([0-9]), hyphens ("-"), underscores
349 ("_"), colons (":"), and periods (".")'. These regexps
350 are slightly over-zealous, in that they remove colons
351 and periods unnecessarily.
352
353 Whitespace is transformed into underscores, and then
354 anything which is not a hyphen or a character that
355 matches \w (alphanumerics and underscore) is removed.
356
357 """
358 # Transform all whitespace to underscore
359 idstring = re.sub(r'\s', "_", '%s' % idstring)
360 # Remove everything that is not a hyphen or a member of \w
361 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
362 return idstring
363
364 def raw_diff(self):
365 """
366 Returns raw string as udiff
367 """
368 udiff_copy = self.copy_iterator()
369 if self.__format == 'gitdiff':
370 udiff_copy = self._parse_gitdiff(udiff_copy)
371 return u''.join(udiff_copy)
372
373 def as_html(self, table_class='code-difftable', line_class='line',
374 new_lineno_class='lineno old', old_lineno_class='lineno new',
375 code_class='code'):
376 """
377 Return udiff as html table with customized css classes
378 """
379 def _link_to_if(condition, label, url):
380 """
381 Generates a link if condition is meet or just the label if not.
382 """
383
384 if condition:
385 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
386 'label': label}
387 else:
388 return label
389 diff_lines = self.prepare()
390 _html_empty = True
391 _html = []
392 _html.append('''<table class="%(table_class)s">\n''' \
393 % {'table_class': table_class})
394 for diff in diff_lines:
395 for line in diff['chunks']:
396 _html_empty = False
397 for change in line:
398 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
399 % {'line_class': line_class,
400 'action': change['action']})
401 anchor_old_id = ''
402 anchor_new_id = ''
403 anchor_old = "%(filename)s_o%(oldline_no)s" % \
404 {'filename': self._safe_id(diff['filename']),
405 'oldline_no': change['old_lineno']}
406 anchor_new = "%(filename)s_n%(oldline_no)s" % \
407 {'filename': self._safe_id(diff['filename']),
408 'oldline_no': change['new_lineno']}
409 cond_old = change['old_lineno'] != '...' and \
410 change['old_lineno']
411 cond_new = change['new_lineno'] != '...' and \
412 change['new_lineno']
413 if cond_old:
414 anchor_old_id = 'id="%s"' % anchor_old
415 if cond_new:
416 anchor_new_id = 'id="%s"' % anchor_new
417 ###########################################################
418 # OLD LINE NUMBER
419 ###########################################################
420 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
421 % {'a_id': anchor_old_id,
422 'old_lineno_cls': old_lineno_class})
423
424 _html.append('''<pre>%(link)s</pre>''' \
425 % {'link':
426 _link_to_if(cond_old, change['old_lineno'], '#%s' \
427 % anchor_old)})
428 _html.append('''</td>\n''')
429 ###########################################################
430 # NEW LINE NUMBER
431 ###########################################################
432
433 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
434 % {'a_id': anchor_new_id,
435 'new_lineno_cls': new_lineno_class})
436
437 _html.append('''<pre>%(link)s</pre>''' \
438 % {'link':
439 _link_to_if(cond_new, change['new_lineno'], '#%s' \
440 % anchor_new)})
441 _html.append('''</td>\n''')
442 ###########################################################
443 # CODE
444 ###########################################################
445 _html.append('''\t<td class="%(code_class)s">''' \
446 % {'code_class': code_class})
447 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
448 % {'code': change['line']})
449 _html.append('''\t</td>''')
450 _html.append('''\n</tr>\n''')
451 _html.append('''</table>''')
452 if _html_empty:
453 return None
454 return ''.join(_html)
455
456 def stat(self):
457 """
458 Returns tuple of adde,and removed lines for this instance
459 """
460 return self.adds, self.removes
@@ -0,0 +1,13 b''
1 import imp
2
3
4 def create_module(name, path):
5 """
6 Returns module created *on the fly*. Returned module would have name same
7 as given ``name`` and would contain code read from file at the given
8 ``path`` (it may also be a zip or package containing *__main__* module).
9 """
10 module = imp.new_module(name)
11 module.__file__ = path
12 execfile(path, module.__dict__)
13 return module
@@ -0,0 +1,28 b''
1 def filesizeformat(bytes, sep=' '):
2 """
3 Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB,
4 102 B, 2.3 GB etc).
5
6 Grabbed from Django (http://www.djangoproject.com), slightly modified.
7
8 :param bytes: size in bytes (as integer)
9 :param sep: string separator between number and abbreviation
10 """
11 try:
12 bytes = float(bytes)
13 except (TypeError, ValueError, UnicodeDecodeError):
14 return '0%sB' % sep
15
16 if bytes < 1024:
17 size = bytes
18 template = '%.0f%sB'
19 elif bytes < 1024 * 1024:
20 size = bytes / 1024
21 template = '%.0f%sKB'
22 elif bytes < 1024 * 1024 * 1024:
23 size = bytes / 1024 / 1024
24 template = '%.1f%sMB'
25 else:
26 size = bytes / 1024 / 1024 / 1024
27 template = '%.2f%sGB'
28 return template % (size, sep)
@@ -0,0 +1,252 b''
1 """
2 Utitlites aimed to help achieve mostly basic tasks.
3 """
4 from __future__ import division
5
6 import re
7 import time
8 import datetime
9 import os.path
10 from subprocess import Popen, PIPE
11 from rhodecode.lib.vcs.exceptions import VCSError
12 from rhodecode.lib.vcs.exceptions import RepositoryError
13 from rhodecode.lib.vcs.utils.paths import abspath
14
15 ALIASES = ['hg', 'git']
16
17
18 def get_scm(path, search_recursively=False, explicit_alias=None):
19 """
20 Returns one of alias from ``ALIASES`` (in order of precedence same as
21 shortcuts given in ``ALIASES``) and top working dir path for the given
22 argument. If no scm-specific directory is found or more than one scm is
23 found at that directory, ``VCSError`` is raised.
24
25 :param search_recursively: if set to ``True``, this function would try to
26 move up to parent directory every time no scm is recognized for the
27 currently checked path. Default: ``False``.
28 :param explicit_alias: can be one of available backend aliases, when given
29 it will return given explicit alias in repositories under more than one
30 version control, if explicit_alias is different than found it will raise
31 VCSError
32 """
33 if not os.path.isdir(path):
34 raise VCSError("Given path %s is not a directory" % path)
35
36 def get_scms(path):
37 return [(scm, path) for scm in get_scms_for_path(path)]
38
39 found_scms = get_scms(path)
40 while not found_scms and search_recursively:
41 newpath = abspath(path, '..')
42 if newpath == path:
43 break
44 path = newpath
45 found_scms = get_scms(path)
46
47 if len(found_scms) > 1:
48 for scm in found_scms:
49 if scm[0] == explicit_alias:
50 return scm
51 raise VCSError('More than one [%s] scm found at given path %s'
52 % (','.join((x[0] for x in found_scms)), path))
53
54 if len(found_scms) is 0:
55 raise VCSError('No scm found at given path %s' % path)
56
57 return found_scms[0]
58
59
60 def get_scms_for_path(path):
61 """
62 Returns all scm's found at the given path. If no scm is recognized
63 - empty list is returned.
64
65 :param path: path to directory which should be checked. May be callable.
66
67 :raises VCSError: if given ``path`` is not a directory
68 """
69 from rhodecode.lib.vcs.backends import get_backend
70 if hasattr(path, '__call__'):
71 path = path()
72 if not os.path.isdir(path):
73 raise VCSError("Given path %r is not a directory" % path)
74
75 result = []
76 for key in ALIASES:
77 dirname = os.path.join(path, '.' + key)
78 if os.path.isdir(dirname):
79 result.append(key)
80 continue
81 # We still need to check if it's not bare repository as
82 # bare repos don't have working directories
83 try:
84 get_backend(key)(path)
85 result.append(key)
86 continue
87 except RepositoryError:
88 # Wrong backend
89 pass
90 except VCSError:
91 # No backend at all
92 pass
93 return result
94
95
96 def get_repo_paths(path):
97 """
98 Returns path's subdirectories which seems to be a repository.
99 """
100 repo_paths = []
101 dirnames = (os.path.abspath(dirname) for dirname in os.listdir(path))
102 for dirname in dirnames:
103 try:
104 get_scm(dirname)
105 repo_paths.append(dirname)
106 except VCSError:
107 pass
108 return repo_paths
109
110
111 def run_command(cmd, *args):
112 """
113 Runs command on the system with given ``args``.
114 """
115 command = ' '.join((cmd, args))
116 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
117 stdout, stderr = p.communicate()
118 return p.retcode, stdout, stderr
119
120
121 def get_highlighted_code(name, code, type='terminal'):
122 """
123 If pygments are available on the system
124 then returned output is colored. Otherwise
125 unchanged content is returned.
126 """
127 import logging
128 try:
129 import pygments
130 pygments
131 except ImportError:
132 return code
133 from pygments import highlight
134 from pygments.lexers import guess_lexer_for_filename, ClassNotFound
135 from pygments.formatters import TerminalFormatter
136
137 try:
138 lexer = guess_lexer_for_filename(name, code)
139 formatter = TerminalFormatter()
140 content = highlight(code, lexer, formatter)
141 except ClassNotFound:
142 logging.debug("Couldn't guess Lexer, will not use pygments.")
143 content = code
144 return content
145
146 def parse_changesets(text):
147 """
148 Returns dictionary with *start*, *main* and *end* ids.
149
150 Examples::
151
152 >>> parse_changesets('aaabbb')
153 {'start': None, 'main': 'aaabbb', 'end': None}
154 >>> parse_changesets('aaabbb..cccddd')
155 {'start': 'aaabbb', 'main': None, 'end': 'cccddd'}
156
157 """
158 text = text.strip()
159 CID_RE = r'[a-zA-Z0-9]+'
160 if not '..' in text:
161 m = re.match(r'^(?P<cid>%s)$' % CID_RE, text)
162 if m:
163 return {
164 'start': None,
165 'main': text,
166 'end': None,
167 }
168 else:
169 RE = r'^(?P<start>%s)?\.{2,3}(?P<end>%s)?$' % (CID_RE, CID_RE)
170 m = re.match(RE, text)
171 if m:
172 result = m.groupdict()
173 result['main'] = None
174 return result
175 raise ValueError("IDs not recognized")
176
177 def parse_datetime(text):
178 """
179 Parses given text and returns ``datetime.datetime`` instance or raises
180 ``ValueError``.
181
182 :param text: string of desired date/datetime or something more verbose,
183 like *yesterday*, *2weeks 3days*, etc.
184 """
185
186 text = text.strip().lower()
187
188 INPUT_FORMATS = (
189 '%Y-%m-%d %H:%M:%S',
190 '%Y-%m-%d %H:%M',
191 '%Y-%m-%d',
192 '%m/%d/%Y %H:%M:%S',
193 '%m/%d/%Y %H:%M',
194 '%m/%d/%Y',
195 '%m/%d/%y %H:%M:%S',
196 '%m/%d/%y %H:%M',
197 '%m/%d/%y',
198 )
199 for format in INPUT_FORMATS:
200 try:
201 return datetime.datetime(*time.strptime(text, format)[:6])
202 except ValueError:
203 pass
204
205 # Try descriptive texts
206 if text == 'tomorrow':
207 future = datetime.datetime.now() + datetime.timedelta(days=1)
208 args = future.timetuple()[:3] + (23, 59, 59)
209 return datetime.datetime(*args)
210 elif text == 'today':
211 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
212 elif text == 'now':
213 return datetime.datetime.now()
214 elif text == 'yesterday':
215 past = datetime.datetime.now() - datetime.timedelta(days=1)
216 return datetime.datetime(*past.timetuple()[:3])
217 else:
218 days = 0
219 matched = re.match(
220 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
221 if matched:
222 groupdict = matched.groupdict()
223 if groupdict['days']:
224 days += int(matched.groupdict()['days'])
225 if groupdict['weeks']:
226 days += int(matched.groupdict()['weeks']) * 7
227 past = datetime.datetime.now() - datetime.timedelta(days=days)
228 return datetime.datetime(*past.timetuple()[:3])
229
230 raise ValueError('Wrong date: "%s"' % text)
231
232
233 def get_dict_for_attrs(obj, attrs):
234 """
235 Returns dictionary for each attribute from given ``obj``.
236 """
237 data = {}
238 for attr in attrs:
239 data[attr] = getattr(obj, attr)
240 return data
241
242
243 def get_total_seconds(timedelta):
244 """
245 Backported for Python 2.5.
246
247 See http://docs.python.org/library/datetime.html.
248 """
249 return ((timedelta.microseconds + (
250 timedelta.seconds +
251 timedelta.days * 24 * 60 * 60
252 ) * 10**6) / 10**6)
@@ -0,0 +1,12 b''
1 """Mercurial libs compatibility
2
3 """
4 from mercurial import archival, merge as hg_merge, patch, ui
5 from mercurial.commands import clone, nullid, pull
6 from mercurial.context import memctx, memfilectx
7 from mercurial.error import RepoError, RepoLookupError, Abort
8 from mercurial.hgweb.common import get_contact
9 from mercurial.localrepo import localrepository
10 from mercurial.match import match
11 from mercurial.mdiff import diffopts
12 from mercurial.node import hex
@@ -0,0 +1,27 b''
1 from rhodecode.lib.vcs.exceptions import VCSError
2
3
4 def import_class(class_path):
5 """
6 Returns class from the given path.
7
8 For example, in order to get class located at
9 ``vcs.backends.hg.MercurialRepository``:
10
11 try:
12 hgrepo = import_class('vcs.backends.hg.MercurialRepository')
13 except VCSError:
14 # hadle error
15 """
16 splitted = class_path.split('.')
17 mod_path = '.'.join(splitted[:-1])
18 class_name = splitted[-1]
19 try:
20 class_mod = __import__(mod_path, {}, {}, [class_name])
21 except ImportError, err:
22 msg = "There was problem while trying to import backend class. "\
23 "Original error was:\n%s" % err
24 raise VCSError(msg)
25 cls = getattr(class_mod, class_name)
26
27 return cls
@@ -0,0 +1,27 b''
1 class LazyProperty(object):
2 """
3 Decorator for easier creation of ``property`` from potentially expensive to
4 calculate attribute of the class.
5
6 Usage::
7
8 class Foo(object):
9 @LazyProperty
10 def bar(self):
11 print 'Calculating self._bar'
12 return 42
13
14 Taken from http://blog.pythonisito.com/2008/08/lazy-descriptors.html and
15 used widely.
16 """
17
18 def __init__(self, func):
19 self._func = func
20 self.__name__ = func.__name__
21 self.__doc__ = func.__doc__
22
23 def __get__(self, obj, klass=None):
24 if obj is None:
25 return None
26 result = obj.__dict__[self.__name__] = self._func(obj)
27 return result
@@ -0,0 +1,72 b''
1 import os
2
3
4 class LockFile(object):
5 """Provides methods to obtain, check for, and release a file based lock which
6 should be used to handle concurrent access to the same file.
7
8 As we are a utility class to be derived from, we only use protected methods.
9
10 Locks will automatically be released on destruction"""
11 __slots__ = ("_file_path", "_owns_lock")
12
13 def __init__(self, file_path):
14 self._file_path = file_path
15 self._owns_lock = False
16
17 def __del__(self):
18 self._release_lock()
19
20 def _lock_file_path(self):
21 """:return: Path to lockfile"""
22 return "%s.lock" % (self._file_path)
23
24 def _has_lock(self):
25 """:return: True if we have a lock and if the lockfile still exists
26 :raise AssertionError: if our lock-file does not exist"""
27 if not self._owns_lock:
28 return False
29
30 return True
31
32 def _obtain_lock_or_raise(self):
33 """Create a lock file as flag for other instances, mark our instance as lock-holder
34
35 :raise IOError: if a lock was already present or a lock file could not be written"""
36 if self._has_lock():
37 return
38 lock_file = self._lock_file_path()
39 if os.path.isfile(lock_file):
40 raise IOError("Lock for file %r did already exist, delete %r in case the lock is illegal" % (self._file_path, lock_file))
41
42 try:
43 fd = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0)
44 os.close(fd)
45 except OSError,e:
46 raise IOError(str(e))
47
48 self._owns_lock = True
49
50 def _obtain_lock(self):
51 """The default implementation will raise if a lock cannot be obtained.
52 Subclasses may override this method to provide a different implementation"""
53 return self._obtain_lock_or_raise()
54
55 def _release_lock(self):
56 """Release our lock if we have one"""
57 if not self._has_lock():
58 return
59
60 # if someone removed our file beforhand, lets just flag this issue
61 # instead of failing, to make it more usable.
62 lfp = self._lock_file_path()
63 try:
64 # on bloody windows, the file needs write permissions to be removable.
65 # Why ...
66 if os.name == 'nt':
67 os.chmod(lfp, 0777)
68 # END handle win32
69 os.remove(lfp)
70 except OSError:
71 pass
72 self._owns_lock = False
@@ -0,0 +1,102 b''
1 """Ordered dict implementation"""
2 from UserDict import DictMixin
3
4
5 class OrderedDict(dict, DictMixin):
6
7 def __init__(self, *args, **kwds):
8 if len(args) > 1:
9 raise TypeError('expected at most 1 arguments, got %d' % len(args))
10 try:
11 self.__end
12 except AttributeError:
13 self.clear()
14 self.update(*args, **kwds)
15
16 def clear(self):
17 self.__end = end = []
18 end += [None, end, end] # sentinel node for doubly linked list
19 self.__map = {} # key --> [key, prev, next]
20 dict.clear(self)
21
22 def __setitem__(self, key, value):
23 if key not in self:
24 end = self.__end
25 curr = end[1]
26 curr[2] = end[1] = self.__map[key] = [key, curr, end]
27 dict.__setitem__(self, key, value)
28
29 def __delitem__(self, key):
30 dict.__delitem__(self, key)
31 key, prev, next = self.__map.pop(key)
32 prev[2] = next
33 next[1] = prev
34
35 def __iter__(self):
36 end = self.__end
37 curr = end[2]
38 while curr is not end:
39 yield curr[0]
40 curr = curr[2]
41
42 def __reversed__(self):
43 end = self.__end
44 curr = end[1]
45 while curr is not end:
46 yield curr[0]
47 curr = curr[1]
48
49 def popitem(self, last=True):
50 if not self:
51 raise KeyError('dictionary is empty')
52 if last:
53 key = reversed(self).next()
54 else:
55 key = iter(self).next()
56 value = self.pop(key)
57 return key, value
58
59 def __reduce__(self):
60 items = [[k, self[k]] for k in self]
61 tmp = self.__map, self.__end
62 del self.__map, self.__end
63 inst_dict = vars(self).copy()
64 self.__map, self.__end = tmp
65 if inst_dict:
66 return (self.__class__, (items,), inst_dict)
67 return self.__class__, (items,)
68
69 def keys(self):
70 return list(self)
71
72 setdefault = DictMixin.setdefault
73 update = DictMixin.update
74 pop = DictMixin.pop
75 values = DictMixin.values
76 items = DictMixin.items
77 iterkeys = DictMixin.iterkeys
78 itervalues = DictMixin.itervalues
79 iteritems = DictMixin.iteritems
80
81 def __repr__(self):
82 if not self:
83 return '%s()' % (self.__class__.__name__,)
84 return '%s(%r)' % (self.__class__.__name__, self.items())
85
86 def copy(self):
87 return self.__class__(self)
88
89 @classmethod
90 def fromkeys(cls, iterable, value=None):
91 d = cls()
92 for key in iterable:
93 d[key] = value
94 return d
95
96 def __eq__(self, other):
97 if isinstance(other, OrderedDict):
98 return len(self) == len(other) and self.items() == other.items()
99 return dict.__eq__(self, other)
100
101 def __ne__(self, other):
102 return not self == other
@@ -0,0 +1,36 b''
1 import os
2
3 abspath = lambda * p: os.path.abspath(os.path.join(*p))
4
5
6 def get_dirs_for_path(*paths):
7 """
8 Returns list of directories, including intermediate.
9 """
10 for path in paths:
11 head = path
12 while head:
13 head, tail = os.path.split(head)
14 if head:
15 yield head
16 else:
17 # We don't need to yield empty path
18 break
19
20
21 def get_dir_size(path):
22 root_path = path
23 size = 0
24 for path, dirs, files in os.walk(root_path):
25 for f in files:
26 try:
27 size += os.path.getsize(os.path.join(path, f))
28 except OSError:
29 pass
30 return size
31
32 def get_user_home():
33 """
34 Returns home path of the user.
35 """
36 return os.getenv('HOME', os.getenv('USERPROFILE'))
@@ -0,0 +1,419 b''
1 # encoding: UTF-8
2 import sys
3 import datetime
4 from string import Template
5 from rhodecode.lib.vcs.utils.filesize import filesizeformat
6 from rhodecode.lib.vcs.utils.helpers import get_total_seconds
7
8
9 class ProgressBarError(Exception):
10 pass
11
12 class AlreadyFinishedError(ProgressBarError):
13 pass
14
15
16 class ProgressBar(object):
17
18 default_elements = ['percentage', 'bar', 'steps']
19
20 def __init__(self, steps=100, stream=None, elements=None):
21 self.step = 0
22 self.steps = steps
23 self.stream = stream or sys.stderr
24 self.bar_char = '='
25 self.width = 50
26 self.separator = ' | '
27 self.elements = elements or self.default_elements
28 self.started = None
29 self.finished = False
30 self.steps_label = 'Step'
31 self.time_label = 'Time'
32 self.eta_label = 'ETA'
33 self.speed_label = 'Speed'
34 self.transfer_label = 'Transfer'
35
36 def __str__(self):
37 return self.get_line()
38
39 def __iter__(self):
40 start = self.step
41 end = self.steps + 1
42 for x in xrange(start, end):
43 self.render(x)
44 yield x
45
46 def get_separator(self):
47 return self.separator
48
49 def get_bar_char(self):
50 return self.bar_char
51
52 def get_bar(self):
53 char = self.get_bar_char()
54 perc = self.get_percentage()
55 length = int(self.width * perc / 100)
56 bar = char * length
57 bar = bar.ljust(self.width)
58 return bar
59
60 def get_elements(self):
61 return self.elements
62
63 def get_template(self):
64 separator = self.get_separator()
65 elements = self.get_elements()
66 return Template(separator.join((('$%s' % e) for e in elements)))
67
68 def get_total_time(self, current_time=None):
69 if current_time is None:
70 current_time = datetime.datetime.now()
71 if not self.started:
72 return datetime.timedelta()
73 return current_time - self.started
74
75 def get_rendered_total_time(self):
76 delta = self.get_total_time()
77 if not delta:
78 ttime = '-'
79 else:
80 ttime = str(delta)
81 return '%s %s' % (self.time_label, ttime)
82
83 def get_eta(self, current_time=None):
84 if current_time is None:
85 current_time = datetime.datetime.now()
86 if self.step == 0:
87 return datetime.timedelta()
88 total_seconds = get_total_seconds(self.get_total_time())
89 eta_seconds = total_seconds * self.steps / self.step - total_seconds
90 return datetime.timedelta(seconds=int(eta_seconds))
91
92 def get_rendered_eta(self):
93 eta = self.get_eta()
94 if not eta:
95 eta = '--:--:--'
96 else:
97 eta = str(eta).rjust(8)
98 return '%s: %s' % (self.eta_label, eta)
99
100 def get_percentage(self):
101 return float(self.step) / self.steps * 100
102
103 def get_rendered_percentage(self):
104 perc = self.get_percentage()
105 return ('%s%%' % (int(perc))).rjust(5)
106
107 def get_rendered_steps(self):
108 return '%s: %s/%s' % (self.steps_label, self.step, self.steps)
109
110 def get_rendered_speed(self, step=None, total_seconds=None):
111 if step is None:
112 step = self.step
113 if total_seconds is None:
114 total_seconds = get_total_seconds(self.get_total_time())
115 if step <= 0 or total_seconds <= 0:
116 speed = '-'
117 else:
118 speed = filesizeformat(float(step) / total_seconds)
119 return '%s: %s/s' % (self.speed_label, speed)
120
121 def get_rendered_transfer(self, step=None, steps=None):
122 if step is None:
123 step = self.step
124 if steps is None:
125 steps = self.steps
126
127 if steps <= 0:
128 return '%s: -' % self.transfer_label
129 total = filesizeformat(float(steps))
130 if step <= 0:
131 transferred = '-'
132 else:
133 transferred = filesizeformat(float(step))
134 return '%s: %s / %s' % (self.transfer_label, transferred, total)
135
136 def get_context(self):
137 return {
138 'percentage': self.get_rendered_percentage(),
139 'bar': self.get_bar(),
140 'steps': self.get_rendered_steps(),
141 'time': self.get_rendered_total_time(),
142 'eta': self.get_rendered_eta(),
143 'speed': self.get_rendered_speed(),
144 'transfer': self.get_rendered_transfer(),
145 }
146
147 def get_line(self):
148 template = self.get_template()
149 context = self.get_context()
150 return template.safe_substitute(**context)
151
152 def write(self, data):
153 self.stream.write(data)
154
155 def render(self, step):
156 if not self.started:
157 self.started = datetime.datetime.now()
158 if self.finished:
159 raise AlreadyFinishedError
160 self.step = step
161 self.write('\r%s' % self)
162 if step == self.steps:
163 self.finished = True
164 if step == self.steps:
165 self.write('\n')
166
167
168 """
169 termcolors.py
170
171 Grabbed from Django (http://www.djangoproject.com)
172 """
173
174 color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
175 foreground = dict([(color_names[x], '3%s' % x) for x in range(8)])
176 background = dict([(color_names[x], '4%s' % x) for x in range(8)])
177
178 RESET = '0'
179 opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'}
180
181 def colorize(text='', opts=(), **kwargs):
182 """
183 Returns your text, enclosed in ANSI graphics codes.
184
185 Depends on the keyword arguments 'fg' and 'bg', and the contents of
186 the opts tuple/list.
187
188 Returns the RESET code if no parameters are given.
189
190 Valid colors:
191 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
192
193 Valid options:
194 'bold'
195 'underscore'
196 'blink'
197 'reverse'
198 'conceal'
199 'noreset' - string will not be auto-terminated with the RESET code
200
201 Examples:
202 colorize('hello', fg='red', bg='blue', opts=('blink',))
203 colorize()
204 colorize('goodbye', opts=('underscore',))
205 print colorize('first line', fg='red', opts=('noreset',))
206 print 'this should be red too'
207 print colorize('and so should this')
208 print 'this should not be red'
209 """
210 code_list = []
211 if text == '' and len(opts) == 1 and opts[0] == 'reset':
212 return '\x1b[%sm' % RESET
213 for k, v in kwargs.iteritems():
214 if k == 'fg':
215 code_list.append(foreground[v])
216 elif k == 'bg':
217 code_list.append(background[v])
218 for o in opts:
219 if o in opt_dict:
220 code_list.append(opt_dict[o])
221 if 'noreset' not in opts:
222 text = text + '\x1b[%sm' % RESET
223 return ('\x1b[%sm' % ';'.join(code_list)) + text
224
225 def make_style(opts=(), **kwargs):
226 """
227 Returns a function with default parameters for colorize()
228
229 Example:
230 bold_red = make_style(opts=('bold',), fg='red')
231 print bold_red('hello')
232 KEYWORD = make_style(fg='yellow')
233 COMMENT = make_style(fg='blue', opts=('bold',))
234 """
235 return lambda text: colorize(text, opts, **kwargs)
236
237 NOCOLOR_PALETTE = 'nocolor'
238 DARK_PALETTE = 'dark'
239 LIGHT_PALETTE = 'light'
240
241 PALETTES = {
242 NOCOLOR_PALETTE: {
243 'ERROR': {},
244 'NOTICE': {},
245 'SQL_FIELD': {},
246 'SQL_COLTYPE': {},
247 'SQL_KEYWORD': {},
248 'SQL_TABLE': {},
249 'HTTP_INFO': {},
250 'HTTP_SUCCESS': {},
251 'HTTP_REDIRECT': {},
252 'HTTP_NOT_MODIFIED': {},
253 'HTTP_BAD_REQUEST': {},
254 'HTTP_NOT_FOUND': {},
255 'HTTP_SERVER_ERROR': {},
256 },
257 DARK_PALETTE: {
258 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
259 'NOTICE': { 'fg': 'red' },
260 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
261 'SQL_COLTYPE': { 'fg': 'green' },
262 'SQL_KEYWORD': { 'fg': 'yellow' },
263 'SQL_TABLE': { 'opts': ('bold',) },
264 'HTTP_INFO': { 'opts': ('bold',) },
265 'HTTP_SUCCESS': { },
266 'HTTP_REDIRECT': { 'fg': 'green' },
267 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' },
268 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
269 'HTTP_NOT_FOUND': { 'fg': 'yellow' },
270 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
271 },
272 LIGHT_PALETTE: {
273 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
274 'NOTICE': { 'fg': 'red' },
275 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
276 'SQL_COLTYPE': { 'fg': 'green' },
277 'SQL_KEYWORD': { 'fg': 'blue' },
278 'SQL_TABLE': { 'opts': ('bold',) },
279 'HTTP_INFO': { 'opts': ('bold',) },
280 'HTTP_SUCCESS': { },
281 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) },
282 'HTTP_NOT_MODIFIED': { 'fg': 'green' },
283 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
284 'HTTP_NOT_FOUND': { 'fg': 'red' },
285 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
286 }
287 }
288 DEFAULT_PALETTE = DARK_PALETTE
289
290 # ---------------------------- #
291 # --- End of termcolors.py --- #
292 # ---------------------------- #
293
294
295 class ColoredProgressBar(ProgressBar):
296
297 BAR_COLORS = (
298 (10, 'red'),
299 (30, 'magenta'),
300 (50, 'yellow'),
301 (99, 'green'),
302 (100, 'blue'),
303 )
304
305 def get_line(self):
306 line = super(ColoredProgressBar, self).get_line()
307 perc = self.get_percentage()
308 if perc > 100:
309 color = 'blue'
310 for max_perc, color in self.BAR_COLORS:
311 if perc <= max_perc:
312 break
313 return colorize(line, fg=color)
314
315
316 class AnimatedProgressBar(ProgressBar):
317
318 def get_bar_char(self):
319 chars = '-/|\\'
320 if self.step >= self.steps:
321 return '='
322 return chars[self.step % len(chars)]
323
324
325 class BarOnlyProgressBar(ProgressBar):
326
327 default_elements = ['bar', 'steps']
328
329 def get_bar(self):
330 bar = super(BarOnlyProgressBar, self).get_bar()
331 perc = self.get_percentage()
332 perc_text = '%s%%' % int(perc)
333 text = (' %s%% ' % (perc_text)).center(self.width, '=')
334 L = text.find(' ')
335 R = text.rfind(' ')
336 bar = ' '.join((bar[:L], perc_text, bar[R:]))
337 return bar
338
339
340 class AnimatedColoredProgressBar(AnimatedProgressBar,
341 ColoredProgressBar):
342 pass
343
344
345 class BarOnlyColoredProgressBar(ColoredProgressBar,
346 BarOnlyProgressBar):
347 pass
348
349
350
351 def main():
352 import time
353
354 print "Standard progress bar..."
355 bar = ProgressBar(30)
356 for x in xrange(1, 31):
357 bar.render(x)
358 time.sleep(0.02)
359 bar.stream.write('\n')
360 print
361
362 print "Empty bar..."
363 bar = ProgressBar(50)
364 bar.render(0)
365 print
366 print
367
368 print "Colored bar..."
369 bar = ColoredProgressBar(20)
370 for x in bar:
371 time.sleep(0.01)
372 print
373
374 print "Animated char bar..."
375 bar = AnimatedProgressBar(20)
376 for x in bar:
377 time.sleep(0.01)
378 print
379
380 print "Animated + colored char bar..."
381 bar = AnimatedColoredProgressBar(20)
382 for x in bar:
383 time.sleep(0.01)
384 print
385
386 print "Bar only ..."
387 bar = BarOnlyProgressBar(20)
388 for x in bar:
389 time.sleep(0.01)
390 print
391
392 print "Colored, longer bar-only, eta, total time ..."
393 bar = BarOnlyColoredProgressBar(40)
394 bar.width = 60
395 bar.elements += ['time', 'eta']
396 for x in bar:
397 time.sleep(0.01)
398 print
399 print
400
401 print "File transfer bar, breaks after 2 seconds ..."
402 total_bytes = 1024 * 1024 * 2
403 bar = ProgressBar(total_bytes)
404 bar.width = 50
405 bar.elements.remove('steps')
406 bar.elements += ['transfer', 'time', 'eta', 'speed']
407 for x in xrange(0, bar.steps, 1024):
408 bar.render(x)
409 time.sleep(0.01)
410 now = datetime.datetime.now()
411 if now - bar.started >= datetime.timedelta(seconds=2):
412 break
413 print
414 print
415
416
417
418 if __name__ == '__main__':
419 main()
@@ -0,0 +1,200 b''
1 """
2 termcolors.py
3
4 Grabbed from Django (http://www.djangoproject.com)
5 """
6
7 color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
8 foreground = dict([(color_names[x], '3%s' % x) for x in range(8)])
9 background = dict([(color_names[x], '4%s' % x) for x in range(8)])
10
11 RESET = '0'
12 opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'}
13
14 def colorize(text='', opts=(), **kwargs):
15 """
16 Returns your text, enclosed in ANSI graphics codes.
17
18 Depends on the keyword arguments 'fg' and 'bg', and the contents of
19 the opts tuple/list.
20
21 Returns the RESET code if no parameters are given.
22
23 Valid colors:
24 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
25
26 Valid options:
27 'bold'
28 'underscore'
29 'blink'
30 'reverse'
31 'conceal'
32 'noreset' - string will not be auto-terminated with the RESET code
33
34 Examples:
35 colorize('hello', fg='red', bg='blue', opts=('blink',))
36 colorize()
37 colorize('goodbye', opts=('underscore',))
38 print colorize('first line', fg='red', opts=('noreset',))
39 print 'this should be red too'
40 print colorize('and so should this')
41 print 'this should not be red'
42 """
43 code_list = []
44 if text == '' and len(opts) == 1 and opts[0] == 'reset':
45 return '\x1b[%sm' % RESET
46 for k, v in kwargs.iteritems():
47 if k == 'fg':
48 code_list.append(foreground[v])
49 elif k == 'bg':
50 code_list.append(background[v])
51 for o in opts:
52 if o in opt_dict:
53 code_list.append(opt_dict[o])
54 if 'noreset' not in opts:
55 text = text + '\x1b[%sm' % RESET
56 return ('\x1b[%sm' % ';'.join(code_list)) + text
57
58 def make_style(opts=(), **kwargs):
59 """
60 Returns a function with default parameters for colorize()
61
62 Example:
63 bold_red = make_style(opts=('bold',), fg='red')
64 print bold_red('hello')
65 KEYWORD = make_style(fg='yellow')
66 COMMENT = make_style(fg='blue', opts=('bold',))
67 """
68 return lambda text: colorize(text, opts, **kwargs)
69
70 NOCOLOR_PALETTE = 'nocolor'
71 DARK_PALETTE = 'dark'
72 LIGHT_PALETTE = 'light'
73
74 PALETTES = {
75 NOCOLOR_PALETTE: {
76 'ERROR': {},
77 'NOTICE': {},
78 'SQL_FIELD': {},
79 'SQL_COLTYPE': {},
80 'SQL_KEYWORD': {},
81 'SQL_TABLE': {},
82 'HTTP_INFO': {},
83 'HTTP_SUCCESS': {},
84 'HTTP_REDIRECT': {},
85 'HTTP_NOT_MODIFIED': {},
86 'HTTP_BAD_REQUEST': {},
87 'HTTP_NOT_FOUND': {},
88 'HTTP_SERVER_ERROR': {},
89 },
90 DARK_PALETTE: {
91 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
92 'NOTICE': { 'fg': 'red' },
93 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
94 'SQL_COLTYPE': { 'fg': 'green' },
95 'SQL_KEYWORD': { 'fg': 'yellow' },
96 'SQL_TABLE': { 'opts': ('bold',) },
97 'HTTP_INFO': { 'opts': ('bold',) },
98 'HTTP_SUCCESS': { },
99 'HTTP_REDIRECT': { 'fg': 'green' },
100 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' },
101 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
102 'HTTP_NOT_FOUND': { 'fg': 'yellow' },
103 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
104 },
105 LIGHT_PALETTE: {
106 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
107 'NOTICE': { 'fg': 'red' },
108 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
109 'SQL_COLTYPE': { 'fg': 'green' },
110 'SQL_KEYWORD': { 'fg': 'blue' },
111 'SQL_TABLE': { 'opts': ('bold',) },
112 'HTTP_INFO': { 'opts': ('bold',) },
113 'HTTP_SUCCESS': { },
114 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) },
115 'HTTP_NOT_MODIFIED': { 'fg': 'green' },
116 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
117 'HTTP_NOT_FOUND': { 'fg': 'red' },
118 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
119 }
120 }
121 DEFAULT_PALETTE = DARK_PALETTE
122
123 def parse_color_setting(config_string):
124 """Parse a DJANGO_COLORS environment variable to produce the system palette
125
126 The general form of a pallete definition is:
127
128 "palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option"
129
130 where:
131 palette is a named palette; one of 'light', 'dark', or 'nocolor'.
132 role is a named style used by Django
133 fg is a background color.
134 bg is a background color.
135 option is a display options.
136
137 Specifying a named palette is the same as manually specifying the individual
138 definitions for each role. Any individual definitions following the pallete
139 definition will augment the base palette definition.
140
141 Valid roles:
142 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table',
143 'http_info', 'http_success', 'http_redirect', 'http_bad_request',
144 'http_not_found', 'http_server_error'
145
146 Valid colors:
147 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
148
149 Valid options:
150 'bold', 'underscore', 'blink', 'reverse', 'conceal'
151
152 """
153 if not config_string:
154 return PALETTES[DEFAULT_PALETTE]
155
156 # Split the color configuration into parts
157 parts = config_string.lower().split(';')
158 palette = PALETTES[NOCOLOR_PALETTE].copy()
159 for part in parts:
160 if part in PALETTES:
161 # A default palette has been specified
162 palette.update(PALETTES[part])
163 elif '=' in part:
164 # Process a palette defining string
165 definition = {}
166
167 # Break the definition into the role,
168 # plus the list of specific instructions.
169 # The role must be in upper case
170 role, instructions = part.split('=')
171 role = role.upper()
172
173 styles = instructions.split(',')
174 styles.reverse()
175
176 # The first instruction can contain a slash
177 # to break apart fg/bg.
178 colors = styles.pop().split('/')
179 colors.reverse()
180 fg = colors.pop()
181 if fg in color_names:
182 definition['fg'] = fg
183 if colors and colors[-1] in color_names:
184 definition['bg'] = colors[-1]
185
186 # All remaining instructions are options
187 opts = tuple(s for s in styles if s in opt_dict.keys())
188 if opts:
189 definition['opts'] = opts
190
191 # The nocolor palette has all available roles.
192 # Use that palette as the basis for determining
193 # if the role is valid.
194 if role in PALETTES[NOCOLOR_PALETTE] and definition:
195 palette[role] = definition
196
197 # If there are no colors specified, return the empty palette.
198 if palette == PALETTES[NOCOLOR_PALETTE]:
199 return None
200 return palette
@@ -17,3 +17,4 b' syntax: regexp'
17 17 ^test\.db$
18 18 ^RhodeCode\.egg-info$
19 19 ^rc\.ini$
20 ^fabfile.py
@@ -10,9 +10,8 b' celery>=2.2.5,<2.3'
10 10 babel
11 11 python-dateutil>=1.5.0,<2.0.0
12 12 dulwich>=0.8.0,<0.9.0
13 vcs>=0.2.3.dev
14 13 webob==1.0.8
15 markdown==2.0.3
14 markdown==2.1.1
16 15 docutils==0.8.1
17 16 py-bcrypt
18 17 mercurial>=2.1,<2.2 No newline at end of file
@@ -49,9 +49,8 b' requirements = ['
49 49 "babel",
50 50 "python-dateutil>=1.5.0,<2.0.0",
51 51 "dulwich>=0.8.0,<0.9.0",
52 "vcs>=0.2.3.dev",
53 52 "webob==1.0.8",
54 "markdown==2.0.3",
53 "markdown==2.1.1",
55 54 "docutils==0.8.1",
56 55 ]
57 56
@@ -37,7 +37,7 b' from rhodecode.lib.base import BaseRepoC'
37 37 from rhodecode.lib.helpers import RepoPage
38 38 from rhodecode.lib.compat import json
39 39
40 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
40 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
41 41 from rhodecode.model.db import Repository
42 42
43 43 log = logging.getLogger(__name__)
@@ -33,9 +33,9 b' from pylons.i18n.translation import _'
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 35
36 from vcs.exceptions import RepositoryError, ChangesetError, \
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 37 ChangesetDoesNotExistError
38 from vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 39
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
@@ -32,11 +32,11 b' from pylons.i18n.translation import _'
32 32 from pylons.controllers.util import redirect
33 33 from pylons.decorators import jsonify
34 34
35 from vcs.conf import settings
36 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
35 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
37 37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
38 38 NodeAlreadyExistsError
39 from vcs.nodes import FileNode
39 from rhodecode.lib.vcs.nodes import FileNode
40 40
41 41 from rhodecode.lib.compat import OrderedDict
42 42 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
@@ -31,7 +31,7 b' from datetime import timedelta, date'
31 31 from itertools import product
32 32 from urlparse import urlparse
33 33
34 from vcs.exceptions import ChangesetError, EmptyRepositoryError, \
34 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
35 35 NodeDoesNotExistError
36 36
37 37 from pylons import tmpl_context as c, request, url, config
@@ -25,7 +25,7 b''
25 25
26 26 import os
27 27 import re
28 from vcs.utils.lazy import LazyProperty
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 29
30 30
31 31 def __get_lem():
@@ -82,7 +82,7 b' ALL_READMES = ['
82 82 # extension together with weights to search lower is first
83 83 RST_EXTS = [
84 84 ('', 0), ('.rst', 1), ('.rest', 1),
85 ('.RST', 2) , ('.REST', 2),
85 ('.RST', 2), ('.REST', 2),
86 86 ('.txt', 3), ('.TXT', 3)
87 87 ]
88 88
@@ -138,7 +138,6 b' def convert_line_endings(line, mode):'
138 138 line = replace(line, '\r\n', '\r')
139 139 line = replace(line, '\n', '\r')
140 140 elif mode == 2:
141 import re
142 141 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
143 142 return line
144 143
@@ -324,7 +323,7 b' def age(curdate):'
324 323 from datetime import datetime
325 324 from webhelpers.date import time_ago_in_words
326 325
327 _ = lambda s:s
326 _ = lambda s: s
328 327
329 328 if not curdate:
330 329 return ''
@@ -341,7 +340,8 b' def age(curdate):'
341 340 pos = 1
342 341 for scale in agescales:
343 342 if scale[1] <= age_seconds:
344 if pos == 6:pos = 5
343 if pos == 6:
344 pos = 5
345 345 return '%s %s' % (time_ago_in_words(curdate,
346 346 agescales[pos][0]), _('ago'))
347 347 pos += 1
@@ -404,8 +404,8 b' def get_changeset_safe(repo, rev):'
404 404 :param repo:
405 405 :param rev:
406 406 """
407 from vcs.backends.base import BaseRepository
408 from vcs.exceptions import RepositoryError
407 from rhodecode.lib.vcs.backends.base import BaseRepository
408 from rhodecode.lib.vcs.exceptions import RepositoryError
409 409 if not isinstance(repo, BaseRepository):
410 410 raise Exception('You must pass an Repository '
411 411 'object as first argument got %s', type(repo))
@@ -427,8 +427,8 b' def get_current_revision(quiet=False):'
427 427 """
428 428
429 429 try:
430 from vcs import get_repo
431 from vcs.utils.helpers import get_scm
430 from rhodecode.lib.vcs import get_repo
431 from rhodecode.lib.vcs.utils.helpers import get_scm
432 432 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
433 433 scm = get_scm(repopath)[0]
434 434 repo = get_repo(path=repopath, alias=scm)
@@ -11,8 +11,8 b''
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13
14 from vcs.exceptions import VCSError
15 from vcs.nodes import FileNode
14 from rhodecode.lib.vcs.exceptions import VCSError
15 from rhodecode.lib.vcs.nodes import FileNode
16 16 from pygments.formatters import HtmlFormatter
17 17 from pygments import highlight
18 18
@@ -34,7 +34,7 b' from pylons import config'
34 34 from hashlib import md5
35 35 from decorator import decorator
36 36
37 from vcs.utils.lazy import LazyProperty
37 from rhodecode.lib.vcs.utils.lazy import LazyProperty
38 38 from rhodecode import CELERY_ON
39 39 from rhodecode.lib import str2bool, safe_str
40 40 from rhodecode.lib.pidlock import DaemonLock, LockHeld
@@ -37,7 +37,7 b' from string import lower'
37 37 from pylons import config, url
38 38 from pylons.i18n.translation import _
39 39
40 from vcs import get_backend
40 from rhodecode.lib.vcs import get_backend
41 41
42 42 from rhodecode import CELERY_ON
43 43 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
@@ -34,10 +34,10 b' from sqlalchemy.ext.hybrid import hybrid'
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 from vcs import get_backend
38 from vcs.utils.helpers import get_scm
39 from vcs.exceptions import VCSError
40 from vcs.utils.lazy import LazyProperty
37 from rhodecode.lib.vcs import get_backend
38 from rhodecode.lib.vcs.utils.helpers import get_scm
39 from rhodecode.lib.vcs.exceptions import VCSError
40 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41 41
42 42 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \
43 43 generate_api_key, safe_unicode
@@ -32,8 +32,8 b' from itertools import tee, imap'
32 32
33 33 from pylons.i18n.translation import _
34 34
35 from vcs.exceptions import VCSError
36 from vcs.nodes import FileNode
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode
37 37
38 38 from rhodecode.lib.utils import EmptyChangeset
39 39
@@ -318,7 +318,7 b' flash = _Flash()'
318 318 #==============================================================================
319 319 # SCM FILTERS available via h.
320 320 #==============================================================================
321 from vcs.utils import author_name, author_email
321 from rhodecode.lib.vcs.utils import author_name, author_email
322 322 from rhodecode.lib import credentials_filter, age as _age
323 323 from rhodecode.model.db import User
324 324
@@ -43,7 +43,7 b' from rhodecode.model.scm import ScmModel'
43 43 from rhodecode.lib import safe_unicode
44 44 from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME
45 45
46 from vcs.exceptions import ChangesetError, RepositoryError, \
46 from rhodecode.lib.vcs.exceptions import ChangesetError, RepositoryError, \
47 47 NodeDoesNotExistError
48 48
49 49 from whoosh.index import create_in, open_dir
@@ -30,6 +30,7 b' import traceback'
30 30
31 31 from dulwich import server as dulserver
32 32
33
33 34 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
34 35
35 36 def handle(self):
@@ -154,7 +154,6 b' class SimpleHg(BaseVCSController):'
154 154 baseui = make_ui('db')
155 155 self.__inject_extras(repo_path, baseui, extras)
156 156
157
158 157 # quick check if that dir exists...
159 158 if is_valid_repo(repo_name, self.basepath) is False:
160 159 return HTTPNotFound()(environ, start_response)
@@ -221,7 +220,6 b' class SimpleHg(BaseVCSController):'
221 220 else:
222 221 return 'pull'
223 222
224
225 223 def __inject_extras(self, repo_path, baseui, extras={}):
226 224 """
227 225 Injects some extra params into baseui instance
@@ -40,11 +40,11 b' from mercurial import ui, config'
40 40
41 41 from webhelpers.text import collapse, remove_formatting, strip_tags
42 42
43 from vcs import get_backend
44 from vcs.backends.base import BaseChangeset
45 from vcs.utils.lazy import LazyProperty
46 from vcs.utils.helpers import get_scm
47 from vcs.exceptions import VCSError
43 from rhodecode.lib.vcs import get_backend
44 from rhodecode.lib.vcs.backends.base import BaseChangeset
45 from rhodecode.lib.vcs.utils.lazy import LazyProperty
46 from rhodecode.lib.vcs.utils.helpers import get_scm
47 from rhodecode.lib.vcs.exceptions import VCSError
48 48
49 49 from rhodecode.lib.caching_query import FromCache
50 50
@@ -34,10 +34,10 b' from sqlalchemy.ext.hybrid import hybrid'
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 from vcs import get_backend
38 from vcs.utils.helpers import get_scm
39 from vcs.exceptions import VCSError
40 from vcs.utils.lazy import LazyProperty
37 from rhodecode.lib.vcs import get_backend
38 from rhodecode.lib.vcs.utils.helpers import get_scm
39 from rhodecode.lib.vcs.exceptions import VCSError
40 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41 41
42 42 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, safe_unicode
43 43 from rhodecode.lib.compat import json
@@ -28,7 +28,7 b' import logging'
28 28 import traceback
29 29 from datetime import datetime
30 30
31 from vcs.backends import get_backend
31 from rhodecode.lib.vcs.backends import get_backend
32 32
33 33 from rhodecode.lib import LazyProperty
34 34 from rhodecode.lib import safe_str, safe_unicode
@@ -28,10 +28,10 b' import traceback'
28 28 import logging
29 29 import cStringIO
30 30
31 from vcs import get_backend
32 from vcs.exceptions import RepositoryError
33 from vcs.utils.lazy import LazyProperty
34 from vcs.nodes import FileNode
31 from rhodecode.lib.vcs import get_backend
32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
34 from rhodecode.lib.vcs.nodes import FileNode
35 35
36 36 from rhodecode import BACKENDS
37 37 from rhodecode.lib import helpers as h
@@ -359,9 +359,9 b' class ScmModel(BaseModel):'
359 359 content, f_path):
360 360
361 361 if repo.alias == 'hg':
362 from vcs.backends.hg import MercurialInMemoryChangeset as IMC
362 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
363 363 elif repo.alias == 'git':
364 from vcs.backends.git import GitInMemoryChangeset as IMC
364 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
365 365
366 366 # decoding here will force that we have proper encoded values
367 367 # in any other case this will throw exceptions and deny commit
@@ -385,9 +385,9 b' class ScmModel(BaseModel):'
385 385 def create_node(self, repo, repo_name, cs, user, author, message, content,
386 386 f_path):
387 387 if repo.alias == 'hg':
388 from vcs.backends.hg import MercurialInMemoryChangeset as IMC
388 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
389 389 elif repo.alias == 'git':
390 from vcs.backends.git import GitInMemoryChangeset as IMC
390 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
391 391 # decoding here will force that we have proper encoded values
392 392 # in any other case this will throw exceptions and deny commit
393 393
@@ -1,18 +1,16 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 import os
4 import vcs
4 from rhodecode.lib import vcs
5 5
6 6 from rhodecode.model.db import Repository
7 7 from rhodecode.tests import *
8 8
9 9 class TestAdminReposController(TestController):
10 10
11
12 11 def __make_repo(self):
13 12 pass
14 13
15
16 14 def test_index(self):
17 15 self.log_user()
18 16 response = self.app.get(url('repos'))
@@ -31,11 +31,10 b''
31 31 import cookielib
32 32 import urllib
33 33 import urllib2
34 import vcs
35 34 import time
36 35
37 36 from os.path import join as jn
38
37 from rhodecode.lib import vcs
39 38
40 39 BASE_URI = 'http://127.0.0.1:5000/%s'
41 40 PROJECT = 'CPython'
@@ -52,7 +51,6 b' o.addheaders = ['
52 51 urllib2.install_opener(o)
53 52
54 53
55
56 54 def test_changelog_walk(pages=100):
57 55 total_time = 0
58 56 for i in range(1, pages):
@@ -67,7 +65,6 b' def test_changelog_walk(pages=100):'
67 65 total_time += e
68 66 print 'visited %s size:%s req:%s ms' % (full_uri, size, e)
69 67
70
71 68 print 'total_time', total_time
72 69 print 'average on req', total_time / float(pages)
73 70
@@ -103,7 +100,7 b' def test_files_walk(limit=100):'
103 100 repo = vcs.get_repo(jn(PROJECT_PATH, PROJECT))
104 101
105 102 from rhodecode.lib.compat import OrderedSet
106 from vcs.exceptions import RepositoryError
103 from rhodecode.lib.vcs.exceptions import RepositoryError
107 104
108 105 paths_ = OrderedSet([''])
109 106 try:
@@ -141,7 +138,6 b' def test_files_walk(limit=100):'
141 138 print 'average on req', total_time / float(cnt)
142 139
143 140
144
145 141 test_changelog_walk(40)
146 142 time.sleep(2)
147 143 test_changeset_walk(limit=100)
@@ -7,10 +7,7 b' from rhodecode import requirements'
7 7 if __py_version__ < (2, 5):
8 8 raise Exception('RhodeCode requires python 2.5 or later')
9 9
10
11 10 dependency_links = [
12 "https://secure.rhodecode.org/vcs/archive/default.zip#egg=vcs-0.2.3.dev",
13 "https://bitbucket.org/marcinkuzminski/vcs/get/default.zip#egg=vcs-0.2.3.dev",
14 11 ]
15 12
16 13 classifiers = [
General Comments 0
You need to be logged in to leave comments. Login now