1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from __future__ import with_statement
18
19 import os.path
20
21 from trac.admin import AdminCommandError, IAdminCommandProvider, get_dir_list
22 from trac.config import ConfigSection, ListOption, Option
23 from trac.core import *
24 from trac.resource import IResourceManager, Resource, ResourceNotFound
25 from trac.util.concurrency import threading
26 from trac.util.datefmt import time_now
27 from trac.util.text import printout, to_unicode, exception_to_unicode
28 from trac.util.translation import _
29 from trac.web.api import IRequestFilter
30 from trac.web.chrome import ITemplateProvider
31
32
34 """Check whether `reponame` is the default repository."""
35 return not reponame or reponame in ('(default)', _('(default)'))
36
37
39 """Exception raised when a repository is invalid."""
40
41
43 """Provide support for a specific version control system."""
44
45 error = None
46
48 """Return the types of version control systems that are supported.
49
50 Yields `(repotype, priority)` pairs, where `repotype` is used to
51 match against the configured `[trac] repository_type` value in TracIni.
52
53 If multiple provider match a given type, the `priority` is used to
54 choose between them (highest number is highest priority).
55
56 If the `priority` returned is negative, this indicates that the
57 connector for the given `repotype` indeed exists but can't be
58 used for some reason. The `error` property can then be used to
59 store an error message or exception relevant to the problem detected.
60 """
61
63 """Return a Repository instance for the given repository type and dir.
64 """
65
66
68 """Provide known named instances of Repository."""
69
71 """Generate repository information for known repositories.
72
73 Repository information is a key,value pair, where the value is
74 a dictionary which must contain at the very least either of
75 the following entries:
76
77 - `'dir'`: the repository directory which can be used by the
78 connector to create a `Repository` instance. This
79 defines a "real" repository.
80
81 - `'alias'`: the name of another repository. This defines an
82 alias to another (real) repository.
83
84 Optional entries:
85
86 - `'type'`: the type of the repository (if not given, the
87 default repository type will be used).
88
89 - `'description'`: a description of the repository (can
90 contain WikiFormatting).
91
92 - `'hidden'`: if set to `'true'`, the repository is hidden
93 from the repository index.
94
95 - `'url'`: the base URL for checking out the repository.
96 """
97
98
100 """Listen for changes in repositories."""
101
103 """Called after a changeset has been added to a repository."""
104
106 """Called after a changeset has been modified in a repository.
107
108 The `old_changeset` argument contains the metadata of the changeset
109 prior to the modification. It is `None` if the old metadata cannot
110 be retrieved.
111 """
112
113
115 """Component providing repositories registered in the DB."""
116
117 implements(IRepositoryProvider, IAdminCommandProvider)
118
119 repository_attrs = ('alias', 'description', 'dir', 'hidden', 'name',
120 'type', 'url')
121
122
123
125 """Retrieve repositories specified in the repository DB table."""
126 repos = {}
127 for id, name, value in self.env.db_query(
128 "SELECT id, name, value FROM repository WHERE name IN (%s)"
129 % ",".join("'%s'" % each for each in self.repository_attrs)):
130 if value is not None:
131 repos.setdefault(id, {})[name] = value
132 reponames = {}
133 for id, info in repos.iteritems():
134 if 'name' in info and ('dir' in info or 'alias' in info):
135 info['id'] = id
136 reponames[info['name']] = info
137 return reponames.iteritems()
138
139
140
142 yield ('repository add', '<repos> <dir> [type]',
143 "Add a source repository",
144 self._complete_add, self._do_add)
145 yield ('repository alias', '<name> <target>',
146 "Create an alias for a repository",
147 self._complete_alias, self._do_alias)
148 yield ('repository remove', '<repos>',
149 "Remove a source repository",
150 self._complete_repos, self._do_remove)
151 yield ('repository set', '<repos> <key> <value>',
152 """Set an attribute of a repository
153
154 The following keys are supported: %s
155 """ % ', '.join(self.repository_attrs),
156 self._complete_set, self._do_set)
157
162
168
172
176
182
183 - def _do_add(self, reponame, dir, type_=None):
185
188
191
192 - def _do_set(self, reponame, key, value):
193 if key not in self.repository_attrs:
194 raise AdminCommandError(_('Invalid key "%(key)s"', key=key))
195 if key == 'dir':
196 value = os.path.abspath(value)
197 self.modify_repository(reponame, {key: value})
198 if not reponame:
199 reponame = '(default)'
200 if key == 'dir':
201 printout(_('You should now run "repository resync %(name)s".',
202 name=reponame))
203 elif key == 'type':
204 printout(_('You may have to run "repository resync %(name)s".',
205 name=reponame))
206
207
208
210 """Add a repository."""
211 if not os.path.isabs(dir):
212 raise TracError(_("The repository directory must be absolute"))
213 if is_default(reponame):
214 reponame = ''
215 rm = RepositoryManager(self.env)
216 if type_ and type_ not in rm.get_supported_types():
217 raise TracError(_("The repository type '%(type)s' is not "
218 "supported", type=type_))
219 with self.env.db_transaction as db:
220 id = rm.get_repository_id(reponame)
221 db.executemany(
222 "INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)",
223 [(id, 'dir', dir),
224 (id, 'type', type_ or '')])
225 rm.reload_repositories()
226
228 """Create an alias repository."""
229 if is_default(reponame):
230 reponame = ''
231 if is_default(target):
232 target = ''
233 rm = RepositoryManager(self.env)
234 repositories = rm.get_all_repositories()
235 if target not in repositories:
236 raise TracError(_("Repository \"%(repo)s\" doesn't exist",
237 repo=target or '(default)'))
238 if 'alias' in repositories[target]:
239 raise TracError(_('Cannot create an alias to the alias "%(repo)s"',
240 repo=target or '(default)'))
241 with self.env.db_transaction as db:
242 id = rm.get_repository_id(reponame)
243 db.executemany(
244 "INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)",
245 [(id, 'dir', None),
246 (id, 'alias', target)])
247 rm.reload_repositories()
248
265
267 """Modify attributes of a repository."""
268 if is_default(reponame):
269 reponame = ''
270 new_reponame = changes.get('name', reponame)
271 if is_default(new_reponame):
272 new_reponame = ''
273 rm = RepositoryManager(self.env)
274 if reponame != new_reponame:
275 repositories = rm.get_all_repositories()
276 if any(reponame == repos.get('alias')
277 for repos in repositories.itervalues()):
278 raise TracError(_('Cannot rename the repository "%(repos)s" '
279 'used in aliases',
280 repos=reponame or '(default)'))
281 with self.env.db_transaction as db:
282 id = rm.get_repository_id(reponame)
283 if reponame != new_reponame:
284 if db("""SELECT id FROM repository WHERE name='name' AND
285 value=%s""", (new_reponame,)):
286 raise TracError(_('The repository "%(name)s" already '
287 'exists.',
288 name=new_reponame or '(default)'))
289 for (k, v) in changes.iteritems():
290 if k not in self.repository_attrs:
291 continue
292 if k in ('alias', 'name') and is_default(v):
293 v = ''
294 if k == 'dir' and not os.path.isabs(v):
295 raise TracError(_("The repository directory must be "
296 "absolute"))
297 db("UPDATE repository SET value=%s WHERE id=%s AND name=%s",
298 (v, id, k))
299 if not db(
300 "SELECT value FROM repository WHERE id=%s AND name=%s",
301 (id, k)):
302 db("""INSERT INTO repository (id, name, value)
303 VALUES (%s, %s, %s)
304 """, (id, k, v))
305 rm.reload_repositories()
306
307
309 """Version control system manager."""
310
311 implements(IRequestFilter, IResourceManager, IRepositoryProvider,
312 ITemplateProvider)
313
314 connectors = ExtensionPoint(IRepositoryConnector)
315 providers = ExtensionPoint(IRepositoryProvider)
316 change_listeners = ExtensionPoint(IRepositoryChangeListener)
317
318 repositories_section = ConfigSection('repositories',
319 """One of the alternatives for registering new repositories is to
320 populate the `[repositories]` section of the `trac.ini`.
321
322 This is especially suited for setting up convenience aliases,
323 short-lived repositories, or during the initial phases of an
324 installation.
325
326 See [TracRepositoryAdmin#ReposTracIni TracRepositoryAdmin] for details
327 about the format adopted for this section and the rest of that page for
328 the other alternatives.
329
330 (''since 0.12'')""")
331
332 repository_type = Option('trac', 'repository_type', 'svn',
333 """Default repository connector type. (''since 0.10'')
334
335 This is also used as the default repository type for repositories
336 defined in the [TracIni#repositories-section repositories] section
337 or using the "Repositories" admin panel. (''since 0.12'')
338 """)
339
340 repository_dir = Option('trac', 'repository_dir', '',
341 """Path to the default repository. This can also be a relative path
342 (''since 0.11'').
343
344 This option is deprecated, and repositories should be defined in the
345 [TracIni#repositories-section repositories] section, or using the
346 "Repositories" admin panel. (''since 0.12'')""")
347
348 repository_sync_per_request = ListOption('trac',
349 'repository_sync_per_request', '(default)',
350 doc="""List of repositories that should be synchronized on every page
351 request.
352
353 Leave this option empty if you have set up post-commit hooks calling
354 `trac-admin $ENV changeset added` on all your repositories
355 (recommended). Otherwise, set it to a comma-separated list of
356 repository names. Note that this will negatively affect performance,
357 and will prevent changeset listeners from receiving events from the
358 repositories specified here. The default is to synchronize the default
359 repository, for backward compatibility. (''since 0.12'')""")
360
362 self._cache = {}
363 self._lock = threading.Lock()
364 self._connectors = None
365 self._all_repositories = None
366
367
368
370 from trac.web.chrome import Chrome, add_warning
371 if handler is not Chrome(self.env):
372 for reponame in self.repository_sync_per_request:
373 start = time_now()
374 if is_default(reponame):
375 reponame = ''
376 try:
377 repo = self.get_repository(reponame)
378 if repo:
379 repo.sync()
380 else:
381 self.log.warning("Unable to find repository '%s' for "
382 "synchronization",
383 reponame or '(default)')
384 continue
385 except TracError, e:
386 add_warning(req,
387 _("Can't synchronize with repository \"%(name)s\" "
388 "(%(error)s). Look in the Trac log for more "
389 "information.", name=reponame or '(default)',
390 error=to_unicode(e)))
391 except Exception, e:
392 add_warning(req,
393 _("Failed to sync with repository \"%(name)s\": "
394 "%(error)s; repository information may be out of "
395 "date. Look in the Trac log for more information "
396 "including mitigation strategies.",
397 name=reponame or '(default)', error=to_unicode(e)))
398 self.log.error(
399 "Failed to sync with repository \"%s\"; You may be "
400 "able to reduce the impact of this issue by "
401 "configuring [trac] repository_sync_per_request; see "
402 "https://trac.edgewall.org/wiki/TracRepositoryAdmin"
403 "#ExplicitSync for more detail: %s",
404 reponame or '(default)',
405 exception_to_unicode(e, traceback=True))
406 self.log.info("Synchronized '%s' repository in %0.2f seconds",
407 reponame or '(default)', time_now() - start)
408 return handler
409
410 - def post_process_request(self, req, template, data, content_type):
411 return (template, data, content_type)
412
413
414
416 yield 'changeset'
417 yield 'source'
418 yield 'repository'
419
421 if resource.realm == 'changeset':
422 parent = resource.parent
423 reponame = parent and parent.id
424 id = resource.id
425 if reponame:
426 return _("Changeset %(rev)s in %(repo)s", rev=id, repo=reponame)
427 else:
428 return _("Changeset %(rev)s", rev=id)
429 elif resource.realm == 'source':
430 parent = resource.parent
431 reponame = parent and parent.id
432 id = resource.id
433 version = ''
434 if format == 'summary':
435 repos = self.get_repository(reponame)
436 node = repos.get_node(resource.id, resource.version)
437 if node.isdir:
438 kind = _("directory")
439 elif node.isfile:
440 kind = _("file")
441 if resource.version:
442 version = _(" at version %(rev)s", rev=resource.version)
443 else:
444 kind = _("path")
445 if resource.version:
446 version = '@%s' % resource.version
447 in_repo = _(" in %(repo)s", repo=reponame) if reponame else ''
448
449 return _('%(kind)s %(id)s%(at_version)s%(in_repo)s',
450 kind=kind, id=id, at_version=version, in_repo=in_repo)
451 elif resource.realm == 'repository':
452 if not resource.id:
453 return _("Default repository")
454 return _("Repository %(repo)s", repo=resource.id)
455
466
489
490
491
493 """Retrieve repositories specified in TracIni.
494
495 The `[repositories]` section can be used to specify a list
496 of repositories.
497 """
498 repositories = self.repositories_section
499 reponames = {}
500
501 if self.repository_dir:
502 reponames[''] = {'dir': self.repository_dir}
503
504 for option in repositories:
505 if option.endswith('.dir') and repositories.get(option):
506 reponames[option[:-4]] = {}
507
508 for option in repositories:
509 alias = repositories.get(option)
510 if '.' not in option:
511 option += '.alias'
512 if option.endswith('.alias') and alias in reponames:
513 reponames.setdefault(option[:-6], {})['alias'] = alias
514
515 for option in repositories:
516 if '.' in option:
517 name, detail = option.rsplit('.', 1)
518 if name in reponames and detail != 'alias':
519 reponames[name][detail] = repositories.get(option)
520
521 for reponame, info in reponames.iteritems():
522 yield (reponame, info)
523
524
525
528
530 from pkg_resources import resource_filename
531 return [resource_filename('trac.versioncontrol', 'templates')]
532
533
534
536 """Return the list of supported repository types."""
537 types = set(type_ for connector in self.connectors
538 for (type_, prio) in connector.get_supported_types() or []
539 if prio >= 0)
540 return list(types)
541
543 """Retrieve the repositories based on the given directory.
544
545 :param directory: the key for identifying the repositories.
546 :return: list of `Repository` instances.
547 """
548 directory = os.path.join(os.path.normcase(directory), '')
549 repositories = []
550 for reponame, repoinfo in self.get_all_repositories().iteritems():
551 dir = repoinfo.get('dir')
552 if dir:
553 dir = os.path.join(os.path.normcase(dir), '')
554 if dir.startswith(directory):
555 repos = self.get_repository(reponame)
556 if repos:
557 repositories.append(repos)
558 return repositories
559
561 """Return a unique id for the given repository name.
562
563 This will create and save a new id if none is found.
564
565 Note: this should probably be renamed as we're dealing
566 exclusively with *db* repository ids here.
567 """
568 with self.env.db_transaction as db:
569 for id, in db(
570 "SELECT id FROM repository WHERE name='name' AND value=%s",
571 (reponame,)):
572 return id
573
574 id = db("SELECT COALESCE(MAX(id), 0) FROM repository")[0][0] + 1
575 db("INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)",
576 (id, 'name', reponame))
577 return id
578
580 """Retrieve the appropriate `Repository` for the given
581 repository name.
582
583 :param reponame: the key for specifying the repository.
584 If no name is given, take the default
585 repository.
586 :return: if no corresponding repository was defined,
587 simply return `None`.
588
589 :raises InvalidRepository: if the repository cannot be opened.
590 """
591 reponame = reponame or ''
592 repoinfo = self.get_all_repositories().get(reponame, {})
593 if 'alias' in repoinfo:
594 reponame = repoinfo['alias']
595 repoinfo = self.get_all_repositories().get(reponame, {})
596 rdir = repoinfo.get('dir')
597 if not rdir:
598 return None
599 rtype = repoinfo.get('type') or self.repository_type
600
601
602 with self.env.db_transaction:
603 with self._lock:
604 tid = threading._get_ident()
605 if tid in self._cache:
606 repositories = self._cache[tid]
607 else:
608 repositories = self._cache[tid] = {}
609 repos = repositories.get(reponame)
610 if not repos:
611 if not os.path.isabs(rdir):
612 rdir = os.path.join(self.env.path, rdir)
613 connector = self._get_connector(rtype)
614 repos = connector.get_repository(rtype, rdir,
615 repoinfo.copy())
616 repositories[reponame] = repos
617 return repos
618
620 """Retrieve a matching `Repository` for the given `path`.
621
622 :param path: the eventually scoped repository-scoped path
623 :return: a `(reponame, repos, path)` triple, where `path` is
624 the remaining part of `path` once the `reponame` has
625 been truncated, if needed.
626 """
627 matches = []
628 path = path.strip('/') + '/' if path else '/'
629 for reponame in self.get_all_repositories().keys():
630 stripped_reponame = reponame.strip('/') + '/'
631 if path.startswith(stripped_reponame):
632 matches.append((len(stripped_reponame), reponame))
633 if matches:
634 matches.sort()
635 length, reponame = matches[-1]
636 path = path[length:]
637 else:
638 reponame = ''
639 return (reponame, self.get_repository(reponame),
640 path.rstrip('/') or '/')
641
643 """Recover the appropriate repository from the current context.
644
645 Lookup the closest source or changeset resource in the context
646 hierarchy and return the name of its associated repository.
647 """
648 while context:
649 if context.resource.realm in ('source', 'changeset') and \
650 context.resource.parent:
651 return context.resource.parent.id
652 context = context.parent
653
655 """Return a dictionary of repository information, indexed by name."""
656 if not self._all_repositories:
657 all_repositories = {}
658 for provider in self.providers:
659 for reponame, info in provider.get_repositories() or []:
660 if reponame in all_repositories:
661 self.log.warn("Discarding duplicate repository '%s'",
662 reponame)
663 else:
664 info['name'] = reponame
665 if 'id' not in info:
666 info['id'] = self.get_repository_id(reponame)
667 all_repositories[reponame] = info
668 self._all_repositories = all_repositories
669 return self._all_repositories
670
672 """Return a set of all real repositories (i.e. excluding aliases)."""
673 repositories = set()
674 for reponame in self.get_all_repositories():
675 try:
676 repos = self.get_repository(reponame)
677 if repos is not None:
678 repositories.add(repos)
679 except TracError:
680 pass
681 return repositories
682
684 """Reload the repositories from the providers."""
685 with self._lock:
686
687 self._cache = {}
688 self._all_repositories = None
689 self.config.touch()
690
691 - def notify(self, event, reponame, revs):
692 """Notify repositories and change listeners about repository events.
693
694 The supported events are the names of the methods defined in the
695 `IRepositoryChangeListener` interface.
696 """
697 self.log.debug("Event %s on repository '%s' for changesets %r",
698 event, reponame or '(default)', revs)
699
700
701
702 repos = self.get_repository(reponame)
703 repositories = []
704 if repos:
705 base = repos.get_base()
706 else:
707 dir = os.path.abspath(reponame)
708 repositories = self.get_repositories_by_dir(dir)
709 if repositories:
710 base = None
711 else:
712 base = reponame
713 if base:
714 repositories = [r for r in self.get_real_repositories()
715 if r.get_base() == base]
716 if not repositories:
717 self.log.warn("Found no repositories matching '%s' base.",
718 base or reponame)
719 return [_("Repository '%(repo)s' not found",
720 repo=reponame or _("(default)"))]
721
722 errors = []
723 for repos in sorted(repositories, key=lambda r: r.reponame):
724 reponame = repos.reponame or '(default)'
725 if reponame in self.repository_sync_per_request:
726 self.log.warn("Repository '%s' should be removed from [trac] "
727 "repository_sync_per_request for explicit "
728 "synchronization", reponame)
729 repos.sync()
730 for rev in revs:
731 args = []
732 if event == 'changeset_modified':
733 try:
734 old_changeset = repos.sync_changeset(rev)
735 except NoSuchChangeset, e:
736 errors.append(exception_to_unicode(e))
737 self.log.warn(
738 "No changeset '%s' found in repository '%s'. "
739 "Skipping subscribers for event %s",
740 rev, reponame, event)
741 continue
742 else:
743 args.append(old_changeset)
744 try:
745 changeset = repos.get_changeset(rev)
746 except NoSuchChangeset:
747 try:
748 repos.sync_changeset(rev)
749 changeset = repos.get_changeset(rev)
750 except NoSuchChangeset, e:
751 errors.append(exception_to_unicode(e))
752 self.log.warn(
753 "No changeset '%s' found in repository '%s'. "
754 "Skipping subscribers for event %s",
755 rev, reponame, event)
756 continue
757 self.log.debug("Event %s on repository '%s' for revision '%s'",
758 event, reponame, rev)
759 for listener in self.change_listeners:
760 getattr(listener, event)(repos, changeset, *args)
761 return errors
762
764 """Free `Repository` instances bound to a given thread identifier"""
765 if tid:
766 assert tid == threading._get_ident()
767 with self._lock:
768 repositories = self._cache.pop(tid, {})
769 for reponame, repos in repositories.iteritems():
770 repos.close()
771
772
773
775 """Retrieve the appropriate connector for the given repository type.
776
777 Note that the self._lock must be held when calling this method.
778 """
779 if self._connectors is None:
780
781 self._connectors = {}
782 for connector in self.connectors:
783 for type_, prio in connector.get_supported_types() or []:
784 keep = (connector, prio)
785 if type_ in self._connectors and \
786 prio <= self._connectors[type_][1]:
787 keep = None
788 if keep:
789 self._connectors[type_] = keep
790 if rtype in self._connectors:
791 connector, prio = self._connectors[rtype]
792 if prio >= 0:
793 return connector
794 else:
795 raise TracError(
796 _('Unsupported version control system "%(name)s"'
797 ': %(error)s', name=rtype,
798 error=to_unicode(connector.error)))
799 else:
800 raise TracError(
801 _('Unsupported version control system "%(name)s": '
802 'Can\'t find an appropriate component, maybe the '
803 'corresponding plugin was not enabled? ', name=rtype))
804
805
812
813
815 - def __init__(self, path, rev, msg=None):
816 if msg is None:
817 msg = _("No node %(path)s at revision %(rev)s", path=path, rev=rev)
818 else:
819 msg = _("%(msg)s: No node %(path)s at revision %(rev)s",
820 msg=msg, path=path, rev=rev)
821 ResourceNotFound.__init__(self, msg, _('No such node'))
822
823
825 """Base class for a repository provided by a version control system."""
826
827 has_linear_changesets = False
828
829 scope = '/'
830
832 """Initialize a repository.
833
834 :param name: a unique name identifying the repository, usually a
835 type-specific prefix followed by the path to the
836 repository.
837 :param params: a `dict` of parameters for the repository. Contains
838 the name of the repository under the key "name" and
839 the surrogate key that identifies the repository in
840 the database under the key "id".
841 :param log: a logger instance.
842
843 :raises InvalidRepository: if the repository cannot be opened.
844 """
845 self.name = name
846 self.params = params
847 self.reponame = params['name']
848 self.id = params['id']
849 self.log = log
850 self.resource = Resource('repository', self.reponame)
851
853 """Close the connection to the repository."""
854 raise NotImplementedError
855
857 """Return the name of the base repository for this repository.
858
859 This function returns the name of the base repository to which scoped
860 repositories belong. For non-scoped repositories, it returns the
861 repository name.
862 """
863 return self.name
864
865 - def clear(self, youngest_rev=None):
866 """Clear any data that may have been cached in instance properties.
867
868 `youngest_rev` can be specified as a way to force the value
869 of the `youngest_rev` property (''will change in 0.12'').
870 """
871 pass
872
873 - def sync(self, rev_callback=None, clean=False):
874 """Perform a sync of the repository cache, if relevant.
875
876 If given, `rev_callback` must be a callable taking a `rev` parameter.
877 The backend will call this function for each `rev` it decided to
878 synchronize, once the synchronization changes are committed to the
879 cache. When `clean` is `True`, the cache is cleaned first.
880 """
881 pass
882
884 """Resync the repository cache for the given `rev`, if relevant.
885
886 Returns a "metadata-only" changeset containing the metadata prior to
887 the resync, or `None` if the old values cannot be retrieved (typically
888 when the repository is not cached).
889 """
890 return None
891
893 """Generate a list of interesting places in the repository.
894
895 `rev` might be used to restrict the list of available locations,
896 but in general it's best to produce all known locations.
897
898 The generated results must be of the form (category, name, path, rev).
899 """
900 return []
901
903 """Return the repository URL for the given path and revision.
904
905 The returned URL can be `None`, meaning that no URL has been specified
906 for the repository, an absolute URL, or a scheme-relative URL starting
907 with `//`, in which case the scheme of the request should be prepended.
908 """
909 return None
910
912 """Retrieve a Changeset corresponding to the given revision `rev`."""
913 raise NotImplementedError
914
916 """Return a globally unique identifier for the ''rev'' changeset.
917
918 Two changesets from different repositories can sometimes refer to
919 the ''very same'' changeset (e.g. the repositories are clones).
920 """
921
923 """Generate Changeset belonging to the given time period (start, stop).
924 """
925 rev = self.youngest_rev
926 while rev:
927 chgset = self.get_changeset(rev)
928 if chgset.date < start:
929 return
930 if chgset.date < stop:
931 yield chgset
932 rev = self.previous_rev(rev)
933
935 """Tell if there's a node at the specified (path,rev) combination.
936
937 When `rev` is `None`, the latest revision is implied.
938 """
939 try:
940 self.get_node(path, rev)
941 return True
942 except TracError:
943 return False
944
946 """Retrieve a Node from the repository at the given path.
947
948 A Node represents a directory or a file at a given revision in the
949 repository.
950 If the `rev` parameter is specified, the Node corresponding to that
951 revision is returned, otherwise the Node corresponding to the youngest
952 revision is returned.
953 """
954 raise NotImplementedError
955
957 """Return the oldest revision stored in the repository."""
958 raise NotImplementedError
959 oldest_rev = property(lambda self: self.get_oldest_rev())
960
962 """Return the youngest revision in the repository."""
963 raise NotImplementedError
964 youngest_rev = property(lambda self: self.get_youngest_rev())
965
967 """Return the revision immediately preceding the specified revision.
968
969 If `path` is given, filter out ancestor revisions having no changes
970 below `path`.
971
972 In presence of multiple parents, this follows the first parent.
973 """
974 raise NotImplementedError
975
977 """Return the revision immediately following the specified revision.
978
979 If `path` is given, filter out descendant revisions having no changes
980 below `path`.
981
982 In presence of multiple children, this follows the first child.
983 """
984 raise NotImplementedError
985
990
992 """Provides a total order over revisions.
993
994 Return `True` if `rev1` is an ancestor of `rev2`.
995 """
996 raise NotImplementedError
997
998 - def get_path_history(self, path, rev=None, limit=None):
999 """Retrieve all the revisions containing this path.
1000
1001 If given, `rev` is used as a starting point (i.e. no revision
1002 ''newer'' than `rev` should be returned).
1003 The result format should be the same as the one of Node.get_history()
1004 """
1005 raise NotImplementedError
1006
1008 """Return a canonical representation of path in the repos."""
1009 raise NotImplementedError
1010
1012 """Return a (unique) canonical representation of a revision.
1013
1014 It's up to the backend to decide which string values of `rev`
1015 (usually provided by the user) should be accepted, and how they
1016 should be normalized. Some backends may for instance want to match
1017 against known tags or branch names.
1018
1019 In addition, if `rev` is `None` or '', the youngest revision should
1020 be returned.
1021
1022 :raise NoSuchChangeset: If the given `rev` isn't found.
1023 """
1024 raise NotImplementedError
1025
1027 """Return a compact representation of a revision in the repos.
1028
1029 :raise NoSuchChangeset: If the given `rev` isn't found.
1030 """
1031 return self.normalize_rev(rev)
1032
1034 """Return a representation of a revision in the repos for displaying to
1035 the user.
1036
1037 This can be a shortened revision string, e.g. for repositories using
1038 long hashes.
1039
1040 :raise NoSuchChangeset: If the given `rev` isn't found.
1041 """
1042 return self.normalize_rev(rev)
1043
1044 - def get_changes(self, old_path, old_rev, new_path, new_rev,
1045 ignore_ancestry=1):
1046 """Generates changes corresponding to generalized diffs.
1047
1048 Generator that yields change tuples (old_node, new_node, kind, change)
1049 for each node change between the two arbitrary (path,rev) pairs.
1050
1051 The old_node is assumed to be None when the change is an ADD,
1052 the new_node is assumed to be None when the change is a DELETE.
1053 """
1054 raise NotImplementedError
1055
1057 """Return True if view permission is granted on the repository."""
1058 return 'BROWSER_VIEW' in perm(self.resource.child('source', '/'))
1059
1060 can_view = is_viewable
1061
1062
1063 -class Node(object):
1064 """Represents a directory or file in the repository at a given revision."""
1065
1066 DIRECTORY = "dir"
1067 FILE = "file"
1068
1069 resource = property(lambda self: Resource('source', self.path,
1070 version=self.rev,
1071 parent=self.repos.resource))
1072
1073
1074
1075
1076
1077
1078 created_rev = None
1079 created_path = None
1080
1081 - def __init__(self, repos, path, rev, kind):
1088
1089 - def get_content(self):
1090 """Return a stream for reading the content of the node.
1091
1092 This method will return `None` for directories.
1093 The returned object must support a `read([len])` method.
1094 """
1095 raise NotImplementedError
1096
1097 - def get_processed_content(self, keyword_substitution=True, eol_hint=None):
1098 """Return a stream for reading the content of the node, with some
1099 standard processing applied.
1100
1101 :param keyword_substitution: if `True`, meta-data keywords
1102 present in the content like ``$Rev$`` are substituted
1103 (which keyword are substituted and how they are
1104 substituted is backend specific)
1105
1106 :param eol_hint: which style of line ending is expected if
1107 `None` was explicitly specified for the file itself in
1108 the version control backend (for example in Subversion,
1109 if it was set to ``'native'``). It can be `None`,
1110 ``'LF'``, ``'CR'`` or ``'CRLF'``.
1111 """
1112 return self.get_content()
1113
1115 """Generator that yields the immediate child entries of a directory.
1116
1117 The entries are returned in no particular order.
1118 If the node is a file, this method returns `None`.
1119 """
1120 raise NotImplementedError
1121
1122 - def get_history(self, limit=None):
1123 """Provide backward history for this Node.
1124
1125 Generator that yields `(path, rev, chg)` tuples, one for each revision
1126 in which the node was changed. This generator will follow copies and
1127 moves of a node (if the underlying version control system supports
1128 that), which will be indicated by the first element of the tuple
1129 (i.e. the path) changing.
1130 Starts with an entry for the current revision.
1131
1132 :param limit: if given, yield at most ``limit`` results.
1133 """
1134 raise NotImplementedError
1135
1137 """Return the change event corresponding to the previous revision.
1138
1139 This returns a `(path, rev, chg)` tuple.
1140 """
1141 skip = True
1142 for p in self.get_history(2):
1143 if skip:
1144 skip = False
1145 else:
1146 return p
1147
1149 """Provide detailed backward history for the content of this Node.
1150
1151 Retrieve an array of revisions, one `rev` for each line of content
1152 for that node.
1153 Only expected to work on (text) FILE nodes, of course.
1154 """
1155 raise NotImplementedError
1156
1158 """Returns the properties (meta-data) of the node, as a dictionary.
1159
1160 The set of properties depends on the version control system.
1161 """
1162 raise NotImplementedError
1163
1165 """The length in bytes of the content.
1166
1167 Will be `None` for a directory.
1168 """
1169 raise NotImplementedError
1170 content_length = property(lambda self: self.get_content_length())
1171
1172 - def get_content_type(self):
1173 """The MIME type corresponding to the content, if known.
1174
1175 Will be `None` for a directory.
1176 """
1177 raise NotImplementedError
1178 content_type = property(lambda self: self.get_content_type())
1179
1181 return self.path.split('/')[-1]
1182 name = property(lambda self: self.get_name())
1183
1185 raise NotImplementedError
1186 last_modified = property(lambda self: self.get_last_modified())
1187
1188 isdir = property(lambda self: self.kind == Node.DIRECTORY)
1189 isfile = property(lambda self: self.kind == Node.FILE)
1190
1192 """Return True if view permission is granted on the node."""
1193 return ('BROWSER_VIEW' if self.isdir else 'FILE_VIEW') \
1194 in perm(self.resource)
1195
1196 can_view = is_viewable
1197
1198
1200 """Represents a set of changes committed at once in a repository."""
1201
1202 ADD = 'add'
1203 COPY = 'copy'
1204 DELETE = 'delete'
1205 EDIT = 'edit'
1206 MOVE = 'move'
1207
1208
1209 DIFF_CHANGES = (EDIT, COPY, MOVE)
1210 OTHER_CHANGES = (ADD, DELETE)
1211 ALL_CHANGES = DIFF_CHANGES + OTHER_CHANGES
1212
1213 resource = property(lambda self: Resource('changeset', self.rev,
1214 parent=self.repos.resource))
1215
1216 - def __init__(self, repos, rev, message, author, date):
1217 self.repos = repos
1218 self.rev = rev
1219 self.message = message or ''
1220 self.author = author or ''
1221 self.date = date
1222
1224 """Returns the properties (meta-data) of the node, as a dictionary.
1225
1226 The set of properties depends on the version control system.
1227
1228 Warning: this used to yield 4-elements tuple (besides `name` and
1229 `text`, there were `wikiflag` and `htmlclass` values).
1230 This is now replaced by the usage of IPropertyRenderer (see #1601).
1231 """
1232 return []
1233
1235 """Generator that produces a tuple for every change in the changeset.
1236
1237 The tuple will contain `(path, kind, change, base_path, base_rev)`,
1238 where `change` can be one of Changeset.ADD, Changeset.COPY,
1239 Changeset.DELETE, Changeset.EDIT or Changeset.MOVE,
1240 and `kind` is one of Node.FILE or Node.DIRECTORY.
1241 The `path` is the targeted path for the `change` (which is
1242 the ''deleted'' path for a DELETE change).
1243 The `base_path` and `base_rev` are the source path and rev for the
1244 action (`None` and `-1` in the case of an ADD change).
1245 """
1246 raise NotImplementedError
1247
1249 """Yield branches to which this changeset belong.
1250 Each branch is given as a pair `(name, head)`, where `name` is
1251 the branch name and `head` a flag set if the changeset is a head
1252 for this branch (i.e. if it has no children changeset).
1253 """
1254 return []
1255
1262
1264 """Return True if view permission is granted on the changeset."""
1265 return 'CHANGESET_VIEW' in perm(self.resource)
1266
1267 can_view = is_viewable
1268
1269
1270
1271
1272
1273
1274