~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/arch/_builtin.py

  • Committer: Robert Collins
  • Date: 2005-09-13 10:46:27 UTC
  • mto: (147.2.6) (364.1.3 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: robertc@robertcollins.net-20050913104627-51f938950a907475
handle inaccessible sibling archives somewhat - note version-0 is still not handled

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# arch-tag: david@allouche.net - 2003-11-17 15:23:30 332029000
 
2
# Copyright (C) 2003 David Allouche <david@allouche.net>
 
3
#
 
4
#    This program is free software; you can redistribute it and/or modify
 
5
#    it under the terms of the GNU General Public License as published by
 
6
#    the Free Software Foundation; either version 2 of the License, or
 
7
#    (at your option) any 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
"""
 
19
Internal module providing top-level arch package names
 
20
 
 
21
This module implements the top-level public interface for the
 
22
arch_ package. But for convenience reasons the author prefers
 
23
to store this code in a file separate from ``__init__.py``.
 
24
 
 
25
.. _arch: arch-module.html
 
26
 
 
27
This module is strictly internal and should never be used.
 
28
 
 
29
:var tla_tells_empty_meta_info: The back-end can to tell empty archive
 
30
    meta-info files.
 
31
 
 
32
    The behaviour of ``tla`` is controlled by the _presence_ of some
 
33
    archive meta-info files, but many releases of tla gave no CLI
 
34
    feature to test for existence of meta-info file: the
 
35
    ``archive-meta-info`` command only gave the content of the file if
 
36
    it was present, and could not differenciate between empty and
 
37
    missing files.
 
38
 
 
39
    There is no cheap way to autodetect what is the behaviour of the
 
40
    currently installed command-line executable, so the behaviour of
 
41
    `arch.Archive` objects is configured by this variables.
 
42
 
 
43
:type tla_tells_empty_meta_info: bool
 
44
 
 
45
:var backend: See `arch.backend`
 
46
 
 
47
:var _arch: Internal deprecated interface to the backend
 
48
"""
 
49
 
 
50
### Package docstring ###
 
51
 
 
52
_package_doc = """\
 
53
High level bindings for the Arch revision control system
 
54
 
 
55
Archive Namespace Class Hierarchy
 
56
---------------------------------
 
57
 
 
58
:group Namespace Classes: Archive, Category, Branch, Version, Revision
 
59
 
 
60
:group Abstract Namespace Classes: NamespaceObject, Setupable, Package,
 
61
    CategoryIterable, BranchIterable, VersionIterable, RevisionIterable,
 
62
    ArchiveItem, CategoryItem, BranchItem, VersionItem
 
63
 
 
64
:group Archive-Related Classes: RevisionFile, NameParser
 
65
 
 
66
The archive namespace classes form a complex hierarchy which
 
67
cannot be appropriately described in individual classes.
 
68
 
 
69
The `Archive`, `Category`, `Branch`, `Version` and `Revision` classes
 
70
model the Arch namespace. Namespace objects can be created without the
 
71
corresponding archive structure being available.
 
72
 
 
73
Since they form a hierarchy of containers with shared methods and
 
74
properties in both directions, but do not have any subclass
 
75
relationship, they are defined using a collection of mixin classes.
 
76
 
 
77
The `RevisionIterable`, `VersionIterable`, `BranchIterable` and
 
78
`CategoryIterable` classes define the features which are inherited by
 
79
enclosing archive containers. Many methods in that hierarchy are
 
80
defined abstract (they raise UnimplementedError). They are always
 
81
overriden and are required to prevent legitimate PyChecker warnings.
 
82
 
 
83
The `ArchiveItem`, `CategoryItem`, `BranchItem` and `VersionItem`
 
84
classes provides features which are inherited by enclosed archive
 
85
items. The `NamespaceObject`, `Setupable` and `Package` classes
 
86
provide miscellaneous features and define aspects which do not fit
 
87
within the rest of the hierarchy.
 
88
 
 
89
::
 
90
 
 
91
  ,--------------------------------> NamespaceObject
 
92
  |                                   / \     / \\
 
93
  |                                    |       |
 
94
  | CategoryIterable <---- Archive ----'       |
 
95
  |         |                              ArchiveItem <-----.
 
96
  |         |                                 / \            |
 
97
  |        \ /                                 |             |
 
98
  | BranchIterable <------ Category -----------+-------> Setupable
 
99
  |         |                                  |              / \\
 
100
  |         |                                  |               |
 
101
  |         |                  ,---------------+---------> Package
 
102
  |        \ /                 |               |           / \  |
 
103
  | VersionIterable <----- Branch ------> CategoryItem      |   |
 
104
  |         |                                 / \           |   |
 
105
            |                                  |            |   |
 
106
  |         |                  ,---------------+------------'   |
 
107
  |        \ /                 |               |                |
 
108
  | RevisionIterable <---- Version -----> BranchItem            |
 
109
  |     |        / \                          / \               |
 
110
  |     |         |                            |                |
 
111
  `-----'         |                            |                |
 
112
                  |       Revision ----> VersionItem            |
 
113
                  |                                             |
 
114
                  `---------------------------------------------'
 
115
 
 
116
 
 
117
This admittedly complicated hierarchy minimizes code duplication and
 
118
provides abstract classes which can be used for testing objects for
 
119
features instead of testing for concrete types.
 
120
 
 
121
 
 
122
:group Source Tree Classes: SourceTree, ForeignTree, ArchSourceTree,
 
123
    LibraryTree, WorkingTree
 
124
 
 
125
:group Changeset and Log Classes: Changeset, Patchlog, LogMessage
 
126
 
 
127
:group Incremental Ouput: ChangesetCreation, ChangesetApplication,
 
128
    Chatter, TreeChange, FileAddition, FileDeletion, FileModification,
 
129
    FilePermissionsChange, FileRename, SymlinkModification,
 
130
    MergeOutcome, PatchConflict
 
131
 
 
132
:group Archive Functions: archives, iter_archives, make_archive,
 
133
    register_archive, get, get_patch, make_continuation
 
134
 
 
135
:group Source Tree Functions: init_tree, in_source_tree, tree_root
 
136
 
 
137
:group User Functions: default_archive, my_id, set_my_id
 
138
 
 
139
:group Changeset Generation Functions: changeset, delta, iter_delta
 
140
 
 
141
:group Pika Escaping Functions: name_escape, name_unescape
 
142
 
 
143
:group Revision Library Functions: register_revision_library,
 
144
    unregister_revision_library, iter_revision_libraries, library_archives,
 
145
    iter_library_archives
 
146
 
 
147
:group Incremental Output Functions: classify_chatter,
 
148
    classify_changeset_creation, classify_changeset_application
 
149
 
 
150
:group Obsolete Utility Functions: filter_archive_logs, filter_revisions,
 
151
    grep_summary, grep_summary_interactive, last_revision, map_name_id,
 
152
    revision_which_created, revisions_merging, suspected_move, temphack
 
153
 
 
154
:var backend: Backend controller.
 
155
 
 
156
    This object is used to configure the backend system: name of the
 
157
    executable, process handling strategy and command-line logging.
 
158
 
 
159
:type backend: `backends.commandline.CommandLineBackend`
 
160
"""
 
161
 
 
162
# Configuration option.
 
163
# If True, expect archive-meta-info exit(1) for missing meta-info.
 
164
# If False, expect it exit(0) and output nothing.
 
165
tla_tells_empty_meta_info = True
 
166
 
 
167
import os
 
168
import shutil
 
169
import itertools
 
170
import re
 
171
from sets import ImmutableSet
 
172
import errors
 
173
import util
 
174
from pathname import PathName, FileName, DirName
 
175
from deprecation import deprecated_usage, deprecated_callable
 
176
from _escaping import *
 
177
from _output import *
 
178
 
 
179
__all__ = []
 
180
 
 
181
def public(*names):
 
182
    __all__.extend(names)
 
183
 
 
184
 
 
185
### Backend abstraction and compatibility
 
186
 
 
187
class _BackendProxy(object):
 
188
    """Internal hack for compatibility with _arch"""
 
189
    def __getattribute__(self, name):
 
190
        return getattr(backend._module, name)
 
191
    def __setattr__(self, name, value):
 
192
        setattr(backend._module, name, value)
 
193
 
 
194
_arch = _BackendProxy()
 
195
 
 
196
from backends import commandline
 
197
 
 
198
backend = commandline.default_backend()
 
199
 
 
200
public('backend')
 
201
 
 
202
 
 
203
### Internal helpers for namespace sanity checks
 
204
 
 
205
class _unsafe(tuple):
 
206
    """Used internally to disable namespace sanity checks"""
 
207
 
 
208
 
 
209
def _archive_param(archive):
 
210
    """Internal argument checking utility"""
 
211
    if isinstance(archive, Archive):
 
212
        return archive.name
 
213
    else:
 
214
        if not NameParser.is_archive_name(archive):
 
215
            raise errors.NamespaceError(archive, 'archive name')
 
216
        return archive
 
217
 
 
218
def _registered_archive_param(archive):
 
219
    """Internal argument checking utility"""
 
220
    archive = _archive_param(archive)
 
221
    if not _arch.archive_registered(archive):
 
222
        raise errors.ArchiveNotRegistered(archive)
 
223
    return archive
 
224
 
 
225
def _archive_limit_param(archive, item):
 
226
    """Internal argument checking utility"""
 
227
    if isinstance(item, ArchiveItem):
 
228
        if archive != item.archive.name:
 
229
            message = "%s is not an item of %s" % (item.fullname, archive)
 
230
            raise ValueError(message)
 
231
        return item.nonarch
 
232
    else:
 
233
        p = NameParser(item)
 
234
        if not p.has_category():
 
235
            raise errors.NamespaceError(item, 'archive limit')
 
236
        if not p.has_archive():
 
237
            return item
 
238
        elif archive != p.get_archive():
 
239
            message = "%s is not an item of %s" % (item.fullname, archive)
 
240
            raise ValueError(message)
 
241
        else:
 
242
            return p.get_nonarch()
 
243
 
 
244
def _version_param(vsn):
 
245
    """Internal argument checking utility"""
 
246
    if isinstance(vsn, Version):
 
247
        return vsn.fullname
 
248
    else:
 
249
        p = NameParser(vsn)
 
250
        if not p.has_archive() or not p.is_version():
 
251
            raise errors.NamespaceError(vsn, 'fully-qualified version')
 
252
        return vsn
 
253
 
 
254
def _revision_param(rev):
 
255
    """Internal argument checking utility."""
 
256
    if isinstance(rev, Revision):
 
257
        return rev.fullname
 
258
    else:
 
259
        p = NameParser(rev)
 
260
        if not p.has_archive() or not p.has_patchlevel():
 
261
            raise errors.NamespaceError(rev, 'fully-qualified revision')
 
262
        return rev
 
263
 
 
264
def _version_revision_param(vsn_rev):
 
265
    """Internal argument checking utility"""
 
266
    if isinstance(vsn_rev, BranchItem):
 
267
        return vsn_rev.fullname
 
268
    else:
 
269
        p = NameParser(vsn_rev)
 
270
        if not p.has_archive() or not p.has_version():
 
271
            raise errors.NamespaceError(vsn_rev,
 
272
                                        'fully-qualified version or revision')
 
273
        return vsn_rev
 
274
 
 
275
def _package_revision_param(pkg_rev):
 
276
    """Internal argument checking utility"""
 
277
    if isinstance(pkg_rev, CategoryItem):
 
278
        return pkg_rev.fullname
 
279
    else:
 
280
        p = NameParser(pkg_rev)
 
281
        if not p.has_archive() or not p.has_package():
 
282
            raise errors.NamespaceError(pkg_rev,
 
283
                                        'fully-qualified package or revision')
 
284
        return pkg_rev
 
285
 
 
286
def _check_version_param(param, name):
 
287
    """Internal argument checking utility"""
 
288
    if not isinstance(param, Version):
 
289
        raise TypeError("Parameter \"%r\" must be a Version, but was: %r" \
 
290
                        % (name, param))
 
291
 
 
292
def _check_revision_param(param, name):
 
293
    """Internal argument checking utility"""
 
294
    if not isinstance(param, Revision):
 
295
        raise TypeError("Parameter \"%s\" must be a Revision"
 
296
                        " but was: %r" % (name, param))
 
297
 
 
298
def _check_relname_param(param, name):
 
299
    """Internal argument checking utility"""
 
300
    if not isinstance(param, basestring):
 
301
        exc_type = TypeError
 
302
    elif os.path.isabs(param):
 
303
        exc_type = ValueError
 
304
    else:
 
305
        return
 
306
    raise exc_type("Parameter \"%s\" must be a relative path (string)"
 
307
                   " but was: %r" % (name, param))
 
308
 
 
309
 
 
310
### Base class for all namespace classes ###
 
311
 
 
312
public('NamespaceObject')
 
313
 
 
314
class NamespaceObject(object):
 
315
 
 
316
    """Base class for all archive objects."""
 
317
 
 
318
    def get_fullname(self):
 
319
        """Deprecated
 
320
 
 
321
        Fully qualified name of this namespace object.
 
322
 
 
323
        :rtype: str
 
324
        :see: `NamespaceObject.fullname`
 
325
        """
 
326
        raise NotImplementedError
 
327
 
 
328
    fullname = property(get_fullname, doc = """
 
329
    Fully qualfied name of this namespace object.
 
330
 
 
331
    :type: str
 
332
    """)
 
333
 
 
334
    def exists(self):
 
335
        """Does this namespace exists?
 
336
 
 
337
        Within the Arch model, history cannot be changed: created archive
 
338
        entries cannot be deleted. However, it is possible to ``unregister`` an
 
339
        archive, or to find references to archives whose location is not known.
 
340
        Thus, existence cannot always be decided. Testing for the existence of
 
341
        a name in a non-registered archive raises
 
342
        `errors.ArchiveNotRegistered`.
 
343
 
 
344
        :return: whether this namespace object exists.
 
345
        :rtype: bool
 
346
        :raise errors.ArchiveNotRegistered: the archive name is not registered,
 
347
            so existence cannot be decided.
 
348
        :raise errors.ExecProblem: there was a problem accessing the archive.
 
349
        """
 
350
        raise NotImplementedError
 
351
 
 
352
    def __repr__(self):
 
353
        """Fully-qualified name in angle brackets.
 
354
 
 
355
        :rtype: str
 
356
        """
 
357
        return '<%s>' % self.fullname
 
358
 
 
359
    def __str__(self):
 
360
        """Fully-qualified name.
 
361
 
 
362
        Returns the value of the fullname attribute.
 
363
 
 
364
        :rtype: str
 
365
        """
 
366
        return self.fullname
 
367
 
 
368
    def __eq__(self, x):
 
369
        """Compare types and fully-qualified names.
 
370
 
 
371
        :return: wether objects have the same types and names.
 
372
        :rtype: bool
 
373
        """
 
374
        return type(self) is type(x) \
 
375
               and self.__class__ is x.__class__ \
 
376
               and self.fullname == x.fullname
 
377
 
 
378
    def __ne__(self, x):
 
379
        """Compare types and fully-qualified names.
 
380
 
 
381
        :return: whether objects have different types or names.
 
382
        :rtype: bool
 
383
        """
 
384
        return type(self) is not type(x) \
 
385
               or self.__class__ is not x.__class__ \
 
386
               or self.fullname != x.fullname
 
387
 
 
388
 
 
389
### Mixins for archive iteration aspect ###
 
390
 
 
391
public(
 
392
    'RevisionIterable',
 
393
    'VersionIterable',
 
394
    'BranchIterable',
 
395
    'CategoryIterable',
 
396
    )
 
397
 
 
398
class RevisionIterable(NamespaceObject):
 
399
 
 
400
    """Abstract class for namespace classes above Revision.
 
401
 
 
402
    RevisionIterable provides features which are common to all objects
 
403
    containing revisions.
 
404
    """
 
405
 
 
406
    def iter_revisions(self, reverse=False):
 
407
        """Iterate over archive revisions.
 
408
 
 
409
        :param reverse: reverse order, recent revisions first.
 
410
        :type reverse: bool
 
411
        :return: all existing revisions in this namespace.
 
412
        :rtype: iterable of `Revision`
 
413
 
 
414
        :precondition: `self.exists()` returns ``True``.
 
415
        """
 
416
        raise NotImplementedError
 
417
 
 
418
    def iter_library_revisions(self, reverse=False):
 
419
        """Iterate over library revisions.
 
420
 
 
421
        :param reverse: reverse order, recent revisions first.
 
422
        :type reverse: bool
 
423
        :return: revisions in this namespace which are present in the
 
424
            revision library.
 
425
        :rtype: iterable of `Revision`
 
426
        """
 
427
        raise NotImplementedError
 
428
 
 
429
 
 
430
class VersionIterable(RevisionIterable):
 
431
 
 
432
    """Abstract class for archive classes above Version.
 
433
 
 
434
    VersionIterable provides features which are common to all objects
 
435
    containing versions.
 
436
    """
 
437
 
 
438
    def iter_versions(self, reverse=False):
 
439
        """Iterate over archive versions.
 
440
 
 
441
        :param reverse: reverse order, higher versions first.
 
442
        :type reverse: bool
 
443
        :return: all existing versions in this namespace.
 
444
        :rtype: iterable of `Version`
 
445
 
 
446
        :precondition: `self.exists()` returns ``True``.
 
447
        """
 
448
        raise NotImplementedError
 
449
 
 
450
    def iter_library_versions(self, reverse=False):
 
451
        """Iterate over library revisions.
 
452
 
 
453
        :param reverse: reverse order, higher versions first.
 
454
        :type reverse: bool
 
455
        :return: versions in this namespace which are present in the
 
456
            revision library.
 
457
        :rtype: iterable of `Version`
 
458
        """
 
459
        raise NotImplementedError
 
460
 
 
461
    def iter_revisions(self, reverse=False):
 
462
        for v in self.iter_versions(reverse):
 
463
            for r in v.iter_revisions(reverse): yield r
 
464
 
 
465
    def iter_library_revisions(self, reverse=False):
 
466
        for v in self.iter_library_versions(reverse):
 
467
            for r in v.iter_library_revisions(reverse): yield r
 
468
 
 
469
 
 
470
class BranchIterable(VersionIterable):
 
471
 
 
472
    """Base class for archive classes above Branch.
 
473
 
 
474
    BranchIterable provides features which are common to all objects
 
475
    containing branches.
 
476
    """
 
477
 
 
478
    def iter_branches(self):
 
479
        """Iterate over archive branches.
 
480
 
 
481
        :return: all existing branches in this namespace.
 
482
        :rtype: iterable of `Branch`
 
483
        :precondition: `self.exists()` returns ``True``.
 
484
        """
 
485
        raise NotImplementedError
 
486
 
 
487
    def iter_library_branches(self):
 
488
        """Iterate over library branches.
 
489
 
 
490
        :return: branches in this namespace which are present in the
 
491
            revision library.
 
492
        :rtype: iterable of `Branch`
 
493
        """
 
494
        raise NotImplementedError
 
495
 
 
496
    def iter_versions(self, reverse=False):
 
497
        for b in self.iter_branches():
 
498
            for v in b.iter_versions(reverse): yield v
 
499
 
 
500
    def iter_library_versions(self, reverse=False):
 
501
        for b in self.iter_library_branches():
 
502
            for v in b.iter_library_versions(reverse): yield v
 
503
 
 
504
 
 
505
class CategoryIterable(BranchIterable):
 
506
 
 
507
    """Base class for Archive.
 
508
 
 
509
    CategoryIterable provides features for the aspect of Archive wich
 
510
    relates to its containing other archive objects.
 
511
    """
 
512
 
 
513
    def iter_categories(self):
 
514
        """Iterate over archive categories.
 
515
 
 
516
        :return: all existing categories in this namespace.
 
517
        :rtype: iterable of `Category`
 
518
        :precondition: `self.exists()` returns ``True``.
 
519
        """
 
520
        raise NotImplementedError
 
521
 
 
522
    def iter_library_categories(self):
 
523
        """Iterate over library categories.
 
524
 
 
525
        :return: categories in this namespace which are present in the
 
526
            revision library.
 
527
        :rtype: iterable of `Category`
 
528
        """
 
529
        raise NotImplementedError
 
530
 
 
531
    def iter_branches(self):
 
532
        for c in self.iter_categories():
 
533
            for b in c.iter_branches(): yield b
 
534
 
 
535
    def iter_library_branches(self):
 
536
        for c in self.iter_library_categories():
 
537
            for b in c.iter_library_branches(): yield b
 
538
 
 
539
 
 
540
### Base classes for archive containment aspect ###
 
541
 
 
542
public(
 
543
    'ArchiveItem',
 
544
    'CategoryItem',
 
545
    'BranchItem',
 
546
    'VersionItem',
 
547
    )
 
548
 
 
549
class ArchiveItem(NamespaceObject):
 
550
 
 
551
    """Base class for all archive components classes.
 
552
 
 
553
    ArchiveItem provides features common to all objects which are
 
554
    structural components of Archive.
 
555
    """
 
556
 
 
557
    def __init__(self, archive, nonarch):
 
558
        self._archive = archive
 
559
        self._nonarch = nonarch
 
560
 
 
561
    def get_archive(self):
 
562
        """Deprecated.
 
563
 
 
564
        Archive which contains this namespace object.
 
565
 
 
566
        :rtype: `Archive`
 
567
        :see: `ArchiveItem.archive`
 
568
        """
 
569
        deprecated_callable(self.get_archive, (type(self), 'archive'))
 
570
        return self.archive
 
571
 
 
572
    def _get_archive(self):
 
573
        return Archive(_unsafe((self._archive,)))
 
574
 
 
575
    archive = property(_get_archive, doc="""
 
576
    Archive which contains this namespace object.
 
577
 
 
578
    :type: `Archive`
 
579
    """)
 
580
 
 
581
    def get_nonarch(self):
 
582
        """Deprecated.
 
583
 
 
584
        Non-arch part of this namespace name.
 
585
 
 
586
        :rtype: str
 
587
        :see: `ArchiveItem.nonarch`
 
588
        """
 
589
        deprecated_callable(self.get_nonarch, (type(self), 'nonarch'))
 
590
        return self.nonarch
 
591
 
 
592
    def _get_nonarch(self):
 
593
        return self._nonarch
 
594
 
 
595
    nonarch = property(_get_nonarch, doc="""
 
596
    Non-arch part of this namespace name.
 
597
 
 
598
    :type: str
 
599
    """)
 
600
 
 
601
    def get_fullname(self):
 
602
        deprecated_callable(self.get_fullname, (type(self), 'fullname'))
 
603
        return self.fullname
 
604
 
 
605
    def _get_fullname(self):
 
606
        return '%s/%s' % (self._archive, self._nonarch)
 
607
 
 
608
    fullname = property(_get_fullname, doc=NamespaceObject.fullname.__doc__)
 
609
 
 
610
 
 
611
class CategoryItem(ArchiveItem):
 
612
 
 
613
    """Base class for archive classes below Category.
 
614
 
 
615
    CategoryItem provides features common to all objects which are
 
616
    structural components of Category.
 
617
    """
 
618
 
 
619
    def get_category(self):
 
620
        """Deprecated.
 
621
 
 
622
        Category which contains this namespace object.
 
623
 
 
624
        :rtype: `Category`
 
625
        :see: `CategoryItem.category`
 
626
        """
 
627
        deprecated_callable(self.get_category, (type(self), 'category'))
 
628
        return self.category
 
629
 
 
630
    def _get_category(self):
 
631
        return Category(_unsafe((self._archive, self._nonarch.split('--')[0])))
 
632
 
 
633
    category = property(_get_category, doc="""
 
634
    Category which contains this object.
 
635
 
 
636
    :type: `Category`
 
637
    """)
 
638
 
 
639
 
 
640
class BranchItem(CategoryItem):
 
641
 
 
642
    """Base class for archive classes Version and Revision.
 
643
 
 
644
    BranchItem provides features common to all objects which are
 
645
    structural components of Branch.
 
646
    """
 
647
 
 
648
    def get_branch(self):
 
649
        """Deprecated.
 
650
 
 
651
        Branch which contains this object.
 
652
 
 
653
        :rtype: `Branch`
 
654
        :see: `BranchItem.branch`
 
655
        """
 
656
        deprecated_callable(self.get_branch, (type(self), 'branch'))
 
657
        return self.branch
 
658
 
 
659
    def _get_branch(self):
 
660
        assert isinstance(self, Version)
 
661
        package = '--'.join(self._nonarch.split('--')[0:-1])
 
662
        return Branch(_unsafe((self._archive, package)))
 
663
 
 
664
    branch = property(_get_branch, doc="""
 
665
    Branch which contains this namespace object.
 
666
 
 
667
    :type: `Branch`
 
668
    """)
 
669
 
 
670
 
 
671
class VersionItem(BranchItem):
 
672
 
 
673
    """Base class for Revision.
 
674
 
 
675
    VersionItem provides features for the aspect of Revision which
 
676
    relate to its containment within other archive objects.
 
677
    """
 
678
 
 
679
    def __init__(self, archive, version, patchlevel):
 
680
        BranchItem.__init__(self, archive, "%s--%s" % (version, patchlevel))
 
681
        self._version, self._patchlevel = version, patchlevel
 
682
 
 
683
    def get_branch(self):
 
684
        deprecated_callable(self.get_branch, (type(self), 'branch'))
 
685
        return self.branch
 
686
 
 
687
    def _get_branch(self):
 
688
        assert isinstance(self, Revision)
 
689
        package = '--'.join(self._version.split('--')[0:-1])
 
690
        return Branch(_unsafe((self._archive, package)))
 
691
 
 
692
    branch = property(_get_branch, doc=BranchItem.branch.__doc__)
 
693
 
 
694
    def get_version(self):
 
695
        """Deprecated.
 
696
 
 
697
        Version which contains this revision.
 
698
 
 
699
        :rtype: `Version`
 
700
        :see: `VersionItem.version`
 
701
        """
 
702
        deprecated_callable(self.get_version, (type(self), 'version'))
 
703
        return self.version
 
704
 
 
705
    def _get_version(self):
 
706
        return Version(_unsafe((self._archive, self._version)))
 
707
 
 
708
    version = property(_get_version, doc="""
 
709
    Version which contains this revision.
 
710
 
 
711
    :type: `Version`
 
712
    """)
 
713
 
 
714
    def get_patchlevel(self):
 
715
        """Deprecated.
 
716
 
 
717
        Patch-level part of this object's name.
 
718
 
 
719
        :rtype: str
 
720
        :see: `VersionItem.patchlevel`
 
721
        """
 
722
        deprecated_callable(self.get_patchlevel, (type(self), 'patchlevel'))
 
723
        return self.patchlevel
 
724
 
 
725
    def _get_patchlevel(self):
 
726
        return self._patchlevel
 
727
 
 
728
    patchlevel = property(_get_patchlevel, doc="""
 
729
    Patch-level part of this object's name.
 
730
 
 
731
    :type: str
 
732
    """)
 
733
 
 
734
 
 
735
### Mixins for misc. features of archive objects ###
 
736
 
 
737
public('Setupable', 'Package')
 
738
 
 
739
class Setupable(ArchiveItem):
 
740
 
 
741
    """Base class for container archive objects."""
 
742
 
 
743
    def setup(self):
 
744
        """Create this namespace in the archive.
 
745
 
 
746
        :precondition: self.archive.exists()
 
747
        :postcondition: `self.exists()`
 
748
        :raise errors.ExecProblem: the archive is not registered or
 
749
            there was a problem accessing the archive
 
750
        """
 
751
        # FIXME: do not do the right thing for anonymous branches
 
752
        _arch.archive_setup(self.fullname)
 
753
 
 
754
 
 
755
class Package(Setupable, RevisionIterable):
 
756
 
 
757
    """Base class for ordered container archive objects."""
 
758
 
 
759
    def as_revision(self):
 
760
        """Deprecated.
 
761
 
 
762
        Latest revision in this package.
 
763
 
 
764
        :rtype: `Revision`
 
765
        :precondition: `self.exists()` returns ``True``
 
766
        :precondition: `self.iter_revisions()` yields at least one object.
 
767
        :raises StopIteration: this package contains no revision
 
768
        :see: `Package.latest_revision`
 
769
        """
 
770
        deprecated_callable(self.as_revision, self.latest_revision)
 
771
        return self.iter_revisions(reverse=True).next()
 
772
 
 
773
    def latest_revision(self):
 
774
        """Latest revision in this package.
 
775
 
 
776
        :rtype: `Revision`
 
777
        :precondition: `self.exists()` returns ``True``
 
778
        :precondition: `self.iter_revisions()` yields at least one object.
 
779
        :raises ValueError: the archive is not registered, or this
 
780
            package does not exist, or it contains no revision.
 
781
        """
 
782
        try:
 
783
            return self.iter_revisions(reverse=True).next()
 
784
        except errors.ExecProblem:
 
785
            try:
 
786
                self_exists = self.exists()
 
787
            except errors.ArchiveNotRegistered:
 
788
                raise ValueError('Archive is not registered: %s'
 
789
                                 % self.archive)
 
790
            if not self_exists:
 
791
                raise ValueError('Package does not exist: %s' % self)
 
792
            raise
 
793
        except StopIteration:
 
794
            raise ValueError('Package contains no revision: %s' % self)
 
795
 
 
796
 
 
797
### Archive classes ###
 
798
 
 
799
public('Archive', 'Category', 'Branch', 'Version', 'Revision')
 
800
 
 
801
class Archive(CategoryIterable):
 
802
 
 
803
    """Arch archive namespace object.
 
804
 
 
805
    In the Arch revision control system, archives are the units of
 
806
    storage. They store revisions organized in categories, branches
 
807
    and versions, and are associated to a `name` and a `location`.
 
808
 
 
809
    :see: `Category`, `Branch`, `Version`, `Revision`
 
810
    """
 
811
 
 
812
    def __init__(self, name):
 
813
        """Create an archive object from its registered name.
 
814
 
 
815
        :param name: archive name, like "jdoe@example.com--2003"
 
816
        :type name: str
 
817
        :raise errors.NamespaceError: invalid archive name.
 
818
        """
 
819
        if isinstance(name, Archive):
 
820
            name = name.__name
 
821
        if isinstance(name, _unsafe):
 
822
            assert len(name) == 1
 
823
            name = name[0]
 
824
        elif not NameParser.is_archive_name(name):
 
825
            raise errors.NamespaceError(name, 'archive name')
 
826
        self.__name = name
 
827
 
 
828
    def get_name(self):
 
829
        """Deprecated.
 
830
 
 
831
        Logical name of the archive.
 
832
 
 
833
        :rtype: str
 
834
        :see: `Archive.name`
 
835
        """
 
836
        deprecated_callable(Archive.get_name, (Archive, 'name'))
 
837
        return self.name
 
838
 
 
839
    def _get_name(self):
 
840
        return self.__name
 
841
 
 
842
    name = property(_get_name, doc="""
 
843
    Logical name of the archive.
 
844
 
 
845
    :type: str
 
846
    """)
 
847
 
 
848
    def get_fullname(self):
 
849
        deprecated_callable(Archive.get_fullname, (Archive, 'fullname'))
 
850
        return self.name
 
851
 
 
852
    fullname = property(_get_name, doc=NamespaceObject.fullname.__doc__)
 
853
 
 
854
    def get_location(self):
 
855
        """Deprecated.
 
856
 
 
857
        URI of the archive, specifies location and access method.
 
858
 
 
859
        :rtype: str
 
860
        :see: `Archive.location`
 
861
        """
 
862
        deprecated_callable(Archive.get_location, (Archive, 'location'))
 
863
        return self.location
 
864
 
 
865
    def _get_location(self):
 
866
        return _arch.whereis_archive(self.name)
 
867
 
 
868
    location = property(_get_location, doc="""
 
869
    URI of the archive, specifies location and access method.
 
870
 
 
871
    For example 'http://ddaa.net/arch/2004', or
 
872
    'sftp://user@sourcecontrol.net/public_html/2004'.
 
873
 
 
874
    :type: str
 
875
    """)
 
876
 
 
877
    def _meta_info(self, key):
 
878
        return _arch.archive_meta_info(self.name, key)
 
879
 
 
880
    def _has_meta_info(self, key):
 
881
        if tla_tells_empty_meta_info:
 
882
            try:
 
883
                _arch.archive_meta_info(self.name, key)
 
884
                return True
 
885
            except KeyError:
 
886
                return False
 
887
        else:
 
888
            return _arch.archive_meta_info(self.name, key) is not None
 
889
 
 
890
    def get_is_mirror(self):
 
891
        """Deprecated.
 
892
 
 
893
        Is this archive registration a mirror?
 
894
 
 
895
        :rtype: bool
 
896
        :see: Archive.is_mirror
 
897
        """
 
898
        deprecated_callable(Archive.get_is_mirror, (Archive, 'is_mirror'))
 
899
        return self.is_mirror
 
900
 
 
901
    def _get_is_mirror(self):
 
902
        return self._has_meta_info('mirror')
 
903
 
 
904
    is_mirror = property(_get_is_mirror, doc="""
 
905
    Is this archive registration a mirror?
 
906
 
 
907
    :type: bool
 
908
    """)
 
909
 
 
910
    def get_official_name(self):
 
911
        """Deprecated.
 
912
 
 
913
        Official archive name of this archive registration.
 
914
 
 
915
        :rtype: str
 
916
        :see: `Archive.official_name`
 
917
        """
 
918
        deprecated_callable(Archive.get_official_name,
 
919
                            (Archive, 'official_name'))
 
920
        return self.official_name
 
921
 
 
922
    def _get_official_name(self):
 
923
        return _arch.archive_meta_info(self.name, 'name')
 
924
 
 
925
    official_name = property(_get_official_name, doc="""
 
926
    Official archive name of this archive registration.
 
927
 
 
928
    :type: str
 
929
    """)
 
930
 
 
931
    def get_is_signed(self):
 
932
        """Deprecated.
 
933
 
 
934
        Is the archive GPG-signed?
 
935
 
 
936
        :rtype: bool
 
937
        :see: `Archive.is_signed`
 
938
        """
 
939
        deprecated_callable(Archive.get_is_signed, (Archive, 'is_signed'))
 
940
        return self.is_signed
 
941
 
 
942
    def _get_is_signed(self):
 
943
        return self._has_meta_info('signed-archive')
 
944
 
 
945
    is_signed = property(_get_is_signed, doc="""
 
946
    Is the archive GPG-signed?
 
947
 
 
948
    :type: bool
 
949
    """)
 
950
 
 
951
    def get_has_listings(self):
 
952
        """Deprecated.
 
953
 
 
954
        Does the archive provide .listing file for http access?
 
955
 
 
956
        :rtype: bool
 
957
        :see: `Archive.has_listings`
 
958
        """
 
959
        deprecated_callable(Archive.get_has_listings,
 
960
                            (Archive, 'has_listings'))
 
961
        return self.has_listings
 
962
 
 
963
    def _get_has_listings(self):
 
964
        return self._has_meta_info('http-blows')
 
965
 
 
966
    has_listings = property(_get_has_listings, doc="""
 
967
    Does the archive provide .listing file for http access?
 
968
 
 
969
    :type: bool
 
970
    """)
 
971
 
 
972
    def _get_version_string(self):
 
973
        location = '/'.join((self.location, '.archive-version'))
 
974
        import urllib
 
975
        version_file = urllib.urlopen(location)
 
976
        try:
 
977
            ret = version_file.read()
 
978
        finally:
 
979
            version_file.close()
 
980
        return ret.rstrip()
 
981
 
 
982
    version_string = property(_get_version_string, doc="""
 
983
    Version string of the archive.
 
984
 
 
985
    Contents of the ``.archive-version`` file at the root of the archive.
 
986
 
 
987
    :type: str
 
988
    """)
 
989
 
 
990
    def __getitem__(self, category):
 
991
        """Instanciate a Category belonging to this archive.
 
992
 
 
993
        :param category: unqualified category name
 
994
        :type category: str
 
995
        :rtype: `Category`
 
996
        """
 
997
        if not NameParser.is_category_name(category):
 
998
            raise errors.NamespaceError(category, 'unqualified category name')
 
999
        return Category(_unsafe((self.name, category)))
 
1000
 
 
1001
    def exists(self):
 
1002
        if _arch.archive_registered(self.name):
 
1003
            return True
 
1004
        else:
 
1005
            raise errors.ArchiveNotRegistered(self.name)
 
1006
 
 
1007
    def is_registered(self):
 
1008
        """Is this archive registered?
 
1009
 
 
1010
        :return: Whether the location associated to this registration name is
 
1011
            known.
 
1012
        :rtype: bool
 
1013
        :see: `register_archive`, `Archive.unregister`
 
1014
        """
 
1015
        return _arch.archive_registered(self.name)
 
1016
 
 
1017
    def iter_categories(self):
 
1018
        for c in _arch.categories(self.name):
 
1019
            yield Category(_unsafe((self.name, c)))
 
1020
 
 
1021
    def iter_library_categories(self):
 
1022
        for c in _arch.library_categories(self.name):
 
1023
            yield Category(_unsafe((self.name, c)))
 
1024
 
 
1025
    def get_categories(self):
 
1026
        """Deprecated.
 
1027
 
 
1028
        Categories in this archive.
 
1029
 
 
1030
        :rtype: tuple of `Category`
 
1031
        :see: `iter_categories`
 
1032
        """
 
1033
        deprecated_callable(Archive.get_categories, Archive.iter_categories)
 
1034
        return tuple(self.iter_categories())
 
1035
 
 
1036
    def _get_categories(self):
 
1037
        deprecated_callable((Archive, 'categories'), Archive.iter_categories)
 
1038
        return tuple(self.iter_categories())
 
1039
 
 
1040
    categories = property(_get_categories, doc="""
 
1041
        Deprecated.
 
1042
 
 
1043
        Categories in this archive.
 
1044
 
 
1045
        :type: tuple of `Category`
 
1046
        :see: `iter_categories`
 
1047
        """)
 
1048
 
 
1049
    def get_library_categories(self):
 
1050
        """Deprecated.
 
1051
 
 
1052
        Categories in this archive  present in the library.
 
1053
 
 
1054
        :rtype: tuple of `Category`
 
1055
        :see: `iter_library_categories`
 
1056
        """
 
1057
        deprecated_callable(Archive.get_library_categories,
 
1058
                            Archive.iter_library_categories)
 
1059
        return tuple(self.iter_library_categories())
 
1060
 
 
1061
    def _get_library_categories(self):
 
1062
        deprecated_callable((Archive, 'library_categories'),
 
1063
                            Archive.iter_library_categories)
 
1064
        return tuple(self.iter_library_categories())
 
1065
 
 
1066
    library_categories = property(_get_library_categories, doc="""
 
1067
    Deprecated.
 
1068
 
 
1069
    Categories in this archive  present in the library.
 
1070
 
 
1071
    :type; tuple of `Category`
 
1072
    :see: `iter_library_categories`
 
1073
    """)
 
1074
 
 
1075
    def unregister(self):
 
1076
        """Unregister this archive.
 
1077
 
 
1078
        :precondition: `self.is_registered()`
 
1079
        :postcondition: not `self.is_registered()`
 
1080
        :see: `register_archive`
 
1081
        """
 
1082
        return _arch.unregister_archive(self.name)
 
1083
 
 
1084
    def make_mirror(self, name, location, signed=False, listing=False):
 
1085
        """Create and register a new mirror of this archive.
 
1086
 
 
1087
        :param name: name of the new mirror (for example
 
1088
            'david@allouche.net--2003b-MIRROR').
 
1089
        :type name: str
 
1090
        :param location: writeable URI were to create the archive mirror.
 
1091
        :type location: str
 
1092
        :param signed: create GPG signatures for the mirror contents
 
1093
        :type signed: bool
 
1094
        :param listing: maintains ''.listing'' files to enable HTTP access.
 
1095
        :type listing: bool
 
1096
 
 
1097
        :return: object for the newly created archive mirror.
 
1098
        :rtype: `Archive`
 
1099
 
 
1100
        :precondition: `self.is_registered()`
 
1101
        :precondition: ``name`` is not a registered archive name
 
1102
        :precondition: ``location`` does not exist and can be created
 
1103
        :postcondition: Archive(name).is_registered()
 
1104
 
 
1105
        :raise errors.NamespaceError: ``name`` is not a valid archive name.
 
1106
        """
 
1107
        a = Archive(name) # check archive name validity first
 
1108
        _arch.make_archive_mirror(self.official_name, name, location,
 
1109
                                  signed, listing)
 
1110
        return a
 
1111
 
 
1112
    def mirror(self, limit=None, fromto=None,
 
1113
               no_cached=False, cached_tags=False):
 
1114
        """Update an archive mirror.
 
1115
 
 
1116
        :param limit: restrict mirrorring to those archive items. All items
 
1117
            must belong to this archive.
 
1118
        :type limit: iterable of at least one ArchiveItem or str
 
1119
 
 
1120
        :param fromto: update the mirror specified by the second item with the
 
1121
            contents of the archive specified by the first item.
 
1122
        :type fromto: sequence of exactly two Archive or str.
 
1123
 
 
1124
        :precondition: If ``fromto`` is provided, both items must be registered
 
1125
            archives names whose official name is this archive.
 
1126
 
 
1127
        :param no_cached: do not copy cached revisions.
 
1128
        :type no_cached: bool
 
1129
 
 
1130
        :param cached_tags: copy only cachedrevs for tags to other archives.
 
1131
        :type cached_tags: bool
 
1132
        """
 
1133
        if limit:
 
1134
            limit = map(lambda x: _archive_limit_param(self.name, x), limit)
 
1135
        if fromto is None:
 
1136
            _arch.archive_mirror(self.name, None, limit=limit,
 
1137
                                 no_cached=no_cached, cached_tags=cached_tags)
 
1138
        if fromto is not None:
 
1139
            from_, to = map(_registered_archive_param, fromto)
 
1140
            _arch.archive_mirror(from_, to, limit=limit,
 
1141
                                 no_cached=no_cached, cached_tags=cached_tags)
 
1142
 
 
1143
 
 
1144
class Category(Setupable, BranchIterable):
 
1145
 
 
1146
    """Arch category namespace object.
 
1147
 
 
1148
    :see: `Archive`, `Branch`, `Version`, `Revision`
 
1149
    """
 
1150
 
 
1151
    def __init__(self, name):
 
1152
        """Create a category object from its name.
 
1153
 
 
1154
        :param name: fully-qualified category name, like
 
1155
          "jdoe@example.com/frob"
 
1156
        :type name: str
 
1157
        :raise errors.NamespaceError: ``name`` is not a valid category name.
 
1158
        """
 
1159
        if isinstance(name, Category):
 
1160
            name = (name._archive, name._nonarch)
 
1161
        elif not isinstance(name, _unsafe):
 
1162
            p = NameParser(name)
 
1163
            if not p.has_archive() or not p.is_category():
 
1164
                raise errors.NamespaceError(name, 'fully-qualified category')
 
1165
            name = (p.get_archive(), p.get_nonarch())
 
1166
        ArchiveItem.__init__(self, *name)
 
1167
 
 
1168
    def __getitem__(self, b):
 
1169
        """Instanciate a branch belonging to this category.
 
1170
 
 
1171
        For example ``Category('jdoe@example.com/frob')['devel']`` is
 
1172
        equivalent to ``Branch('jdoe@example.com/frob--devel')``.
 
1173
 
 
1174
        :param b: branch id.
 
1175
        :type b: str
 
1176
        :rtype: `Category`
 
1177
 
 
1178
        :raise NamespaceError: argument is not a valid branch id.
 
1179
        """
 
1180
        if b is not None and not NameParser.is_branch_name(b):
 
1181
            raise errors.NamespaceError(b, 'unqualified branch name')
 
1182
        if b is None:                   # nameless branch
 
1183
            return Branch(_unsafe((self._archive, self._nonarch)))
 
1184
        else:                           # named branch
 
1185
            return Branch(_unsafe((self._archive,
 
1186
                                   "%s--%s" % (self._nonarch, b))))
 
1187
 
 
1188
    def exists(self):
 
1189
        return _arch.category_exists(self._archive, self._nonarch)
 
1190
 
 
1191
    def iter_branches(self):
 
1192
        for b in _arch.branches(self.fullname):
 
1193
            yield Branch(_unsafe((self._archive, b)))
 
1194
 
 
1195
    def iter_library_branches(self):
 
1196
        for b in _arch.library_branches(self.fullname):
 
1197
            yield Branch(_unsafe((self._archive, b)))
 
1198
 
 
1199
    def get_branches(self):
 
1200
        """Deprecated.
 
1201
 
 
1202
        Branches in this category.
 
1203
 
 
1204
        :rtype: tuple of `Branch`
 
1205
        :see: `iter_branches`
 
1206
        """
 
1207
        deprecated_callable(self.get_branches, self.iter_branches)
 
1208
        return tuple(self.iter_branches())
 
1209
 
 
1210
    def _get_branches(self):
 
1211
        deprecated_callable((type(self), 'branches'), self.iter_branches)
 
1212
        return tuple(self.iter_branches())
 
1213
 
 
1214
    branches = property(_get_branches, doc="""
 
1215
    Deprecated.
 
1216
 
 
1217
    Branches in this category.
 
1218
 
 
1219
    :type: tuple of `Branch`
 
1220
    :see: `Category.iter_branches`
 
1221
    """)
 
1222
 
 
1223
    def get_library_branches(self):
 
1224
        """Deprecated.
 
1225
 
 
1226
        Branches in this category present in the library.
 
1227
 
 
1228
        :rtype: tuple of `Branch`
 
1229
        :see: `iter_library_branches`
 
1230
        """
 
1231
        deprecated_callable(self.get_library_branches,
 
1232
                            self.iter_library_branches)
 
1233
        return tuple(self.iter_library_branches())
 
1234
 
 
1235
    def _get_library_branches(self):
 
1236
        deprecated_callable((Category, 'library_branches'),
 
1237
                            self.iter_library_branches)
 
1238
        return tuple(self.iter_library_branches())
 
1239
 
 
1240
    library_branches = property(_get_library_branches, doc="""
 
1241
    Deprecated.
 
1242
 
 
1243
    Branches in this category present in the library.
 
1244
 
 
1245
    :type: tuple of `Branch`
 
1246
    :see: `Category.iter_branches`
 
1247
    """)
 
1248
 
 
1249
 
 
1250
class Branch(CategoryItem, Package, VersionIterable):
 
1251
 
 
1252
    """Arch branch namespace object.
 
1253
 
 
1254
    :see: `Archive`, `Category`, `Version`, `Revision`
 
1255
    """
 
1256
 
 
1257
    def __init__(self, name):
 
1258
        """Create a Branch object from its name.
 
1259
 
 
1260
        :param name: fully-qualified branch name, like
 
1261
            "jdoe@example.com--2004/frob--devo" or
 
1262
            "jdoe@example.com--2004/frob".
 
1263
        :type name: str
 
1264
        :raise errors.NamespaceError: ``name`` is not a valid branch name.
 
1265
        """
 
1266
        if isinstance(name, Branch):
 
1267
            name = (name._archive, name._nonarch)
 
1268
        elif not isinstance(name, _unsafe):
 
1269
            p = NameParser(name)
 
1270
            if not p.has_archive() or not p.is_package():
 
1271
                raise errors.NamespaceError(name, 'fully-qualified branch')
 
1272
            assert p.is_package()
 
1273
            name = (p.get_archive(), p.get_nonarch())
 
1274
        CategoryItem.__init__(self, *name)
 
1275
 
 
1276
    def __getitem__(self, v):
 
1277
        """Instanciate a version belonging to this branch.
 
1278
 
 
1279
        For example ``Branch('jdoe@example.com/frob--devel')['0']`` is
 
1280
        equivalent to ``Branch('jdoe@example.com/frob--devel--0')``.
 
1281
 
 
1282
        :param v: branch id.
 
1283
        :type v: str
 
1284
        :rtype: `Version`
 
1285
 
 
1286
        :raise NamespaceError: argument is not a valid version id.
 
1287
        """
 
1288
        if not NameParser.is_version_id(v):
 
1289
            raise errors.NamespaceError(v, 'version id')
 
1290
        return Version(_unsafe((self._archive, "%s--%s" % (self._nonarch, v))))
 
1291
 
 
1292
    def exists(self):
 
1293
        return _arch.branch_exists(self._archive, self._nonarch)
 
1294
 
 
1295
    def iter_versions(self, reverse=False):
 
1296
        for v in _arch.versions(self.fullname, reverse):
 
1297
            yield Version(_unsafe((self._archive, v)))
 
1298
 
 
1299
    def iter_library_versions(self, reverse=False):
 
1300
        for v in _arch.library_versions(self.fullname, reverse):
 
1301
            yield Version(_unsafe((self._archive, v)))
 
1302
 
 
1303
    def get_versions(self, reverse=False):
 
1304
        """Deprecated.
 
1305
 
 
1306
        Versions in this branch.
 
1307
 
 
1308
        :rtype: tuple of `Version`
 
1309
        :see: `iter_versions`
 
1310
        """
 
1311
        deprecated_callable(self.get_versions, self.iter_versions)
 
1312
        return tuple(self.iter_versions(reverse))
 
1313
 
 
1314
    def _get_versions(self):
 
1315
        deprecated_callable((Branch, 'versions'), self.iter_versions)
 
1316
        return tuple(self.iter_versions(reverse=False))
 
1317
 
 
1318
    versions = property(_get_versions, doc="""
 
1319
    Deprecated.
 
1320
 
 
1321
    Versions in this branch.
 
1322
 
 
1323
    :type: tuple of `Version`
 
1324
    :see: `iter_versions`
 
1325
    """)
 
1326
 
 
1327
    def get_library_versions(self, reverse=False):
 
1328
        """Deprecated.
 
1329
 
 
1330
        Versions in this branch present in the library.
 
1331
 
 
1332
        :rtype: tuple of `Version`
 
1333
        :see: `iter_library_versions`
 
1334
        """
 
1335
        deprecated_callable(self.get_library_versions,
 
1336
                            self.iter_library_versions)
 
1337
        return tuple(self.iter_library_versions(reverse))
 
1338
 
 
1339
    def _get_library_versions(self):
 
1340
        deprecated_callable((Branch, 'library_versions'),
 
1341
                            self.iter_library_versions)
 
1342
        return tuple(self.iter_library_versions(reverse=False))
 
1343
 
 
1344
    library_versions = property(_get_library_versions, doc="""
 
1345
    Deprecated.
 
1346
 
 
1347
    Versions in this branch present in the library.
 
1348
 
 
1349
    :type: tuple of `Version`
 
1350
    :see: `iter_library_versions`
 
1351
    """)
 
1352
 
 
1353
    def as_version(self):
 
1354
        """Deprecated.
 
1355
 
 
1356
        Latest version in this branch.
 
1357
 
 
1358
        :rtype: `Version`
 
1359
        :precondition: `self.exists()` returns ``True``
 
1360
        :precondition: `self.iter_versions` yields at least one object.
 
1361
        :raise IndexError: this branch is empty.
 
1362
        :see: `latest_version`
 
1363
        """
 
1364
        deprecated_callable(self.as_version, self.latest_version)
 
1365
        return list(self.iter_versions(reverse=True))[0]
 
1366
 
 
1367
    def latest_version(self):
 
1368
        """Latest version in this branch.
 
1369
 
 
1370
        :rtype: `Version`
 
1371
        :precondition: `self.exists()` returns ``True``
 
1372
        :precondition: `self.iter_versions` yields at least one object.
 
1373
        :raise ValueError: the archive is not registered, or this branch does
 
1374
            not exist, or it contains no version.
 
1375
        """
 
1376
        try:
 
1377
            return self.iter_versions(reverse=True).next()
 
1378
        except errors.ExecProblem:
 
1379
            try:
 
1380
                self_exists = self.exists()
 
1381
            except errors.ArchiveNotRegistered:
 
1382
                raise ValueError('Archive is not registered: %s'
 
1383
                                 % self.archive)
 
1384
            if not self_exists:
 
1385
                raise ValueError('Branch does not exist: %s' % self)
 
1386
            raise
 
1387
        except StopIteration:
 
1388
            raise ValueError('Branch contains no version: %s' % self)
 
1389
 
 
1390
 
 
1391
 
 
1392
class Version(BranchItem, Package, RevisionIterable):
 
1393
 
 
1394
    """Arch version namespace object.
 
1395
 
 
1396
    :see: `Archive`, `Category`, `Branch`, `Revision`
 
1397
    """
 
1398
 
 
1399
    def __init__(self, name):
 
1400
        """Create a Version object from its name.
 
1401
 
 
1402
        :param name: fully-qualified version name, like
 
1403
           "jdoe@example.com--2004/frob--devo--1.2".
 
1404
        :type name: str
 
1405
 
 
1406
        :note: Nameless branches have no "branch" part in their name.
 
1407
        """
 
1408
        if isinstance(name, Version):
 
1409
            name = (name._archive, name._nonarch)
 
1410
        elif not isinstance(name, _unsafe):
 
1411
            p = NameParser(name)
 
1412
            if not p.has_archive() or not p.is_version():
 
1413
                raise errors.NamespaceError(name, 'fully-qualified version')
 
1414
            name = (p.get_archive(), p.get_nonarch())
 
1415
        BranchItem.__init__(self, *name)
 
1416
 
 
1417
    def __getitem__(self, lvl):
 
1418
        """Instanciate a revision belonging to this version.
 
1419
 
 
1420
        For example ``Version('jdoe@example.com/frob--devel--0')['patch-1']``
 
1421
        is equivalent to
 
1422
        ``Revision('jdoe@example.com/frob--devel--0--patch1')``.
 
1423
 
 
1424
        :param lvl: patch level.
 
1425
        :type lvl: str
 
1426
        :rtype: `Revision`
 
1427
 
 
1428
        :raise NamespaceError: argument is not a valid version patchlevel.
 
1429
        """
 
1430
        if not NameParser.is_patchlevel(lvl):
 
1431
            raise errors.NamespaceError(lvl, 'unqualified patchlevel')
 
1432
        return Revision(_unsafe((self._archive, self._nonarch, lvl)))
 
1433
 
 
1434
    def as_version(self):
 
1435
        """Deprecated.
 
1436
 
 
1437
        This version.
 
1438
 
 
1439
        :rtype: `Version`
 
1440
        """
 
1441
        deprecated_callable(self.as_version, because='Foolish consistency.')
 
1442
        return self
 
1443
 
 
1444
    def exists(self):
 
1445
        return _arch.version_exists(self._archive, self._nonarch)
 
1446
 
 
1447
    def iter_revisions(self, reverse=False):
 
1448
        for lvl in _arch.revisions(self.fullname, reverse):
 
1449
            yield Revision(_unsafe((self._archive, self._nonarch, lvl)))
 
1450
 
 
1451
    def iter_library_revisions(self, reverse=False):
 
1452
        for lvl in _arch.library_revisions(self.fullname, reverse):
 
1453
            yield Revision(_unsafe((self._archive, self._nonarch, lvl)))
 
1454
 
 
1455
    def get_revisions(self, reverse=False):
 
1456
        """Deprecated.
 
1457
 
 
1458
        Revisions in this version.
 
1459
 
 
1460
        :rtype: tuple of `Revision`
 
1461
        :see: `iter_revisions`
 
1462
        """
 
1463
        deprecated_callable(self.get_revisions, self.iter_revisions)
 
1464
        return tuple(self.iter_revisions(reverse))
 
1465
 
 
1466
    def _get_revisions(self):
 
1467
        deprecated_callable((Version, 'revisions'), self.iter_revisions)
 
1468
        return tuple(self.iter_revisions(reverse=False))
 
1469
 
 
1470
    revisions = property(_get_revisions, doc="""
 
1471
    Deprecated.
 
1472
 
 
1473
    Revisions in this version.
 
1474
 
 
1475
    :type: tuple of `Revision`
 
1476
    :see: `iter_revisions`
 
1477
    """)
 
1478
 
 
1479
    def get_library_revisions(self, reverse=False):
 
1480
        """Deprecated.
 
1481
 
 
1482
        Revisions in this version present in the library.
 
1483
 
 
1484
        :rtype: tuple of `Revision`
 
1485
        :see: `iter_library_revisions`
 
1486
        """
 
1487
        deprecated_callable(self.get_library_revisions,
 
1488
                            self.iter_library_revisions)
 
1489
        return tuple(self.iter_library_revisions(reverse))
 
1490
 
 
1491
    def _get_library_revisions(self):
 
1492
        deprecated_callable((Version, 'library_revisions'),
 
1493
                            self.iter_library_revisions)
 
1494
        return tuple(self.iter_library_revisions(reverse=False))
 
1495
 
 
1496
    library_revisions = property(_get_library_revisions, doc="""
 
1497
    Deprecated.
 
1498
 
 
1499
    Revisions in this version present in the library.
 
1500
 
 
1501
    :type: tuple of `Revision`
 
1502
    :see: `iter_library_revisions`
 
1503
    """)
 
1504
 
 
1505
    def iter_merges(self, other=None, reverse=False, metoo=True):
 
1506
        """Iterate over merge points in this version.
 
1507
 
 
1508
        This method is mostly useful to save multiple invocations of
 
1509
        the command-line tool and multiple connection to a remote
 
1510
        archives when building an ancestry graph. Ideally, it would
 
1511
        not be present and the desired merge graph traversal would be
 
1512
        done using the new_patches and merges properties of Patchlog
 
1513
        objects.
 
1514
 
 
1515
        :param other: list merges with that version.
 
1516
        :type other: `Version`
 
1517
        :param reverse: reverse order, recent revisions first.
 
1518
        :type reverse: bool
 
1519
        :param metoo: do not report the presence of a patch within itself
 
1520
        :type metoo: bool
 
1521
 
 
1522
        :return: Iterator of tuples (R, T) where R are revisions in this
 
1523
            version and T are iterable of revisions in the ``other`` version.
 
1524
        :rtype: iterable of `Revision`
 
1525
        """
 
1526
        if other is None: othername = None
 
1527
        else: othername = other.fullname
 
1528
        flow = _arch.iter_merges(self.fullname, othername, reverse, metoo)
 
1529
        last_target = None
 
1530
        sources = []
 
1531
        for target, source in flow:
 
1532
            if last_target is None:
 
1533
                last_target = target
 
1534
            if target == last_target:
 
1535
                sources.append(source)
 
1536
            else:
 
1537
                yield self[last_target], itertools.imap(Revision, sources)
 
1538
                last_target = target
 
1539
                sources = [source]
 
1540
        yield self[last_target], itertools.imap(Revision, sources)
 
1541
 
 
1542
    def iter_cachedrevs(self):
 
1543
        """Iterate over the cached revisions in this version.
 
1544
 
 
1545
        :rtype: iterator of `Revision`
 
1546
        """
 
1547
        for rev in _arch.cachedrevs(self.fullname):
 
1548
            lvl = rev.split('--')[-1]
 
1549
            yield self[lvl]
 
1550
 
 
1551
 
 
1552
class Revision(VersionItem):
 
1553
 
 
1554
    """Arch revision namespace object.
 
1555
 
 
1556
    :see: `Archive`, `Category`, `Branch`, `Version`
 
1557
    :group Libray Methods: library_add, library_remove, library_find
 
1558
    :group History Methods: get_ancestor, get_previous, iter_ancestors
 
1559
    """
 
1560
 
 
1561
    def __init__(self, name):
 
1562
        """Create a Revision object from its name.
 
1563
 
 
1564
        :param name: fully-qualified revision, like
 
1565
            "jdoe@example.com--2004/frob--devo--1.2--patch-2".
 
1566
        :type name: str
 
1567
 
 
1568
        :note: Nameless branches have no "branch" part in their name.
 
1569
        """
 
1570
        if isinstance(name, Revision):
 
1571
            name = (name._archive, name._version, name._patchlevel)
 
1572
        elif not isinstance(name, _unsafe):
 
1573
            p = NameParser(name)
 
1574
            if not p.has_archive() or not p.has_patchlevel():
 
1575
                raise errors.NamespaceError(name, 'fully-qualified revision')
 
1576
            name = (p.get_archive(), p.get_package_version(),
 
1577
                    p.get_patchlevel())
 
1578
        VersionItem.__init__(self, *name)
 
1579
        self.__patchlog = Patchlog(self)
 
1580
 
 
1581
    def as_revision(self):
 
1582
        """Deprecated
 
1583
 
 
1584
        Returns this revision. For consistency with `Package.as_revision()`.
 
1585
 
 
1586
        :rtype: `Revision`
 
1587
        """
 
1588
        deprecated_callable(self.as_revision, because='Foolish consistency.')
 
1589
        return self
 
1590
 
 
1591
    def exists(self):
 
1592
        return _arch.revision_exists \
 
1593
               (self._archive, self._version, self._patchlevel)
 
1594
 
 
1595
    def get(self, dir, link=False):
 
1596
        """Construct a project tree for this revision.
 
1597
 
 
1598
        Extract this revision from the archive.
 
1599
 
 
1600
        :param dir: path of the project tree to create. Must not
 
1601
            already exist.
 
1602
        :type dir: str
 
1603
        :param link: hardlink files to revision library instead of copying
 
1604
        :type link: bool
 
1605
        :return: newly created project tree.
 
1606
        :rtype: `WorkingTree`
 
1607
        """
 
1608
        _arch.get(self.fullname, dir, link)
 
1609
        return WorkingTree(dir)
 
1610
 
 
1611
    def get_patch(self, dir):
 
1612
        """Fetch the changeset associated to this revision.
 
1613
 
 
1614
        :param dir: name of the changeset directory to create. Must
 
1615
            not already exist.
 
1616
        :type dir: str
 
1617
        :return: changeset associated to this revision.
 
1618
        :rtype: `Changeset`
 
1619
        """
 
1620
        _arch.get_patch(self.fullname, dir)
 
1621
        return Changeset(dir)
 
1622
 
 
1623
    def get_patchlog(self):
 
1624
        """Deprecated.
 
1625
 
 
1626
        Patchlog associated to this revision.
 
1627
 
 
1628
        :rtype: `Patchlog`
 
1629
        :see: `Revision.patchlog`
 
1630
        """
 
1631
        deprecated_callable(self.get_patchlog, (Revision, 'patchlog'))
 
1632
        return self.patchlog
 
1633
 
 
1634
    def _get_patchlog(self):
 
1635
        return self.__patchlog
 
1636
 
 
1637
    patchlog = property(_get_patchlog, doc="""
 
1638
    Patchlog associated to this revision.
 
1639
 
 
1640
    The `Patchlog` object is created in `__init__`, since log parsing is
 
1641
    deferred that has little overhead and avoid parsing the log for a given
 
1642
    revision several times. The patchlog data is read from the archive.
 
1643
 
 
1644
    :type: `Patchlog`
 
1645
    :see: `ArchSourceTree.iter_logs`
 
1646
    """)
 
1647
 
 
1648
    def make_continuation(self, target):
 
1649
        """Create a continuation of this revision in the target version.
 
1650
 
 
1651
        :param target: version to create a continuation into. If it does not
 
1652
            exist yet, it is created.
 
1653
        :type target: Version
 
1654
        """
 
1655
        _check_version_param(target, 'target')
 
1656
        target_name = target.fullname
 
1657
        _arch.archive_setup(target_name)
 
1658
        _arch.tag(self.fullname, target_name)
 
1659
 
 
1660
    def library_add(self, library=None):
 
1661
        """Add this revision to the library.
 
1662
 
 
1663
        :postcondition: self in self.version.iter_library_revisions()
 
1664
        """
 
1665
        _arch.library_add(self.fullname, library)
 
1666
 
 
1667
    def library_remove(self):
 
1668
        """Remove this revision from the library.
 
1669
 
 
1670
        :precondition: self in self.version.iter_library_revisions()
 
1671
        :postcondition: self not in self.version.iter_library_revisions()
 
1672
        """
 
1673
        _arch.library_remove(self.fullname)
 
1674
 
 
1675
    def library_find(self):
 
1676
        """The copy of this revision in the library.
 
1677
 
 
1678
        :rtype: `LibraryTree`
 
1679
        :precondition: self in self.version.iter_library_revisions()
 
1680
        """
 
1681
        return LibraryTree(_arch.library_find(self.fullname))
 
1682
 
 
1683
    #def library_file(self, file):
 
1684
    #    raise NotImplementedError, "library_file is not yet implemented"
 
1685
 
 
1686
    def get_ancestor(self):
 
1687
        """Deprecated.
 
1688
 
 
1689
        Parent revision.
 
1690
 
 
1691
        :return:
 
1692
            - The previous namespace revision, if this revision is regular
 
1693
              commit.
 
1694
            - The tag origin, if this revision is a continuation
 
1695
            - ``None`` if this revision is an import.
 
1696
 
 
1697
        :rtype: `Revision` or None
 
1698
        :see: `Revision.ancestor`
 
1699
        """
 
1700
        deprecated_callable(self.get_ancestor, (Revision, 'ancestor'))
 
1701
        return self.ancestor
 
1702
 
 
1703
    def _get_ancestor(self):
 
1704
        rvsn = _arch.ancestor(self.fullname)
 
1705
        if rvsn is None:
 
1706
            return None
 
1707
        else:
 
1708
            return Revision(rvsn)
 
1709
 
 
1710
    ancestor = property(_get_ancestor, doc="""
 
1711
    Parent revision.
 
1712
 
 
1713
    - The previous namespace revision, if this revision is regular commit.
 
1714
    - The tag origin, if this revision is a continuation
 
1715
    - ``None`` if this revision is an import.
 
1716
 
 
1717
    :type: `Revision` or None
 
1718
    """)
 
1719
 
 
1720
    def get_previous(self):
 
1721
        """Deprecated.
 
1722
 
 
1723
        Previous namespace revision.
 
1724
 
 
1725
        :return: the previous revision in the same version, or None if this
 
1726
            revision is a ``base-0``.
 
1727
        :rtype: `Revision` or None
 
1728
        :see: `Revision.previous`
 
1729
        """
 
1730
        deprecated_callable(self.get_previous, (Revision, 'previous'))
 
1731
        return self.previous
 
1732
 
 
1733
    def _get_previous(self):
 
1734
        rvsn = _arch.previous(self.fullname)
 
1735
        if rvsn is None: return None
 
1736
        else: return Revision(rvsn)
 
1737
 
 
1738
    previous = property(_get_previous, doc="""
 
1739
    Previous namespace revision.
 
1740
 
 
1741
    The previous revision in the same version, or None if this revision is a
 
1742
    ``base-0``.
 
1743
 
 
1744
    :type: `Revision` or None
 
1745
    """)
 
1746
 
 
1747
    def iter_ancestors(self, metoo=False):
 
1748
        """Ancestor revisions.
 
1749
 
 
1750
        :param metoo: yield ``self`` as the first revision.
 
1751
        :type metoo: bool
 
1752
        :return: all the revisions in that line of development.
 
1753
        :rtype: iterator of `Revision`
 
1754
        """
 
1755
        i = _arch.iter_ancestry_graph(self.fullname)
 
1756
        if not metoo: i.next() # the first revision is self
 
1757
        for ancest, merge in i:
 
1758
            yield Revision(ancest)
 
1759
 
 
1760
    def cache(self, cache=None):
 
1761
        """Cache a full source tree for this revision in its archive.
 
1762
 
 
1763
        :param cache: cache root for trees with pristines.
 
1764
        :type cache: bool
 
1765
        """
 
1766
        _arch.cacherev(self.fullname, cache)
 
1767
 
 
1768
    def uncache(self):
 
1769
        """Remove the cached tree of this revision from its archive."""
 
1770
        _arch.uncacherev(self.fullname)
 
1771
 
 
1772
    def _file_url(self, name):
 
1773
        return '/'.join([self.archive.location, self.category.nonarch,
 
1774
                         self.branch.nonarch, self.version.nonarch,
 
1775
                         self.patchlevel, name])
 
1776
 
 
1777
    _checksum_regex = re.compile(
 
1778
        "^Signature-for: (.*)/(.*)\n"
 
1779
        "([a-zA-Z0-9]+ [^/\\s]+ [a-fA-F0-9]+\n)*",
 
1780
        re.MULTILINE)
 
1781
 
 
1782
    def _parse_checksum(self, text):
 
1783
        match = self._checksum_regex.search(text)
 
1784
        checksum_body = match.group()
 
1785
        checksum_lines = checksum_body.strip().split('\n')
 
1786
        checksum_words = map(lambda x: x.split(), checksum_lines)
 
1787
        assert checksum_words[0] == ['Signature-for:', self.fullname]
 
1788
        del checksum_words[0]
 
1789
        checksums = {}
 
1790
        for algo, name, value in checksum_words:
 
1791
            name_sums = checksums.setdefault(name, dict())
 
1792
            name_sums[algo] = value
 
1793
        return checksums
 
1794
 
 
1795
    def iter_files(self):
 
1796
        """Files stored in the archive for that revision.
 
1797
 
 
1798
        :rtype: iterable of `RevisionFile`
 
1799
        """
 
1800
        import urllib
 
1801
        for checksum_name, can_fail in \
 
1802
                (('checksum', False), ('checksum.cacherev', True)):
 
1803
            try:
 
1804
                checksum_file = urllib.urlopen(self._file_url(checksum_name))
 
1805
            except IOError:
 
1806
                if can_fail: continue
 
1807
                else: raise
 
1808
            try:
 
1809
                checksum_data = checksum_file.read()
 
1810
            finally:
 
1811
                checksum_file.close()
 
1812
            yield RevisionFile(self, checksum_name, {})
 
1813
            checksums = self._parse_checksum(checksum_data)
 
1814
            for name, name_sums in checksums.items():
 
1815
                yield RevisionFile(self, name, name_sums)
 
1816
 
 
1817
 
 
1818
public('RevisionFile')
 
1819
 
 
1820
class RevisionFile(object):
 
1821
 
 
1822
    """File component of an archived revision.
 
1823
 
 
1824
    :ivar revision: revision this file belongs to.
 
1825
    :type revision: `Revision`
 
1826
    :ivar name: name of that file.
 
1827
    :type name: str
 
1828
    :ivar checksums: dictionnary whose keys are checksum algorithms
 
1829
        (e.g. ``"md5"``, ``"sha1"``) and whose values are checksum
 
1830
        values (strings of hexadecimal digits).
 
1831
    :type checksums: dict of str to str
 
1832
    """
 
1833
 
 
1834
    def __init__(self, revision, name, checksums):
 
1835
        self.revision = revision
 
1836
        self.name = name
 
1837
        self.checksums = checksums
 
1838
 
 
1839
    def _get_data(self):
 
1840
        import urllib
 
1841
        my_file = urllib.urlopen(self.revision._file_url(self.name))
 
1842
        try:
 
1843
            ret = my_file.read()
 
1844
        finally:
 
1845
            my_file.close()
 
1846
        return ret
 
1847
 
 
1848
    data = property(_get_data,
 
1849
                    """Content of of that file.
 
1850
 
 
1851
                    :type: str
 
1852
                    """)
 
1853
 
 
1854
 
 
1855
### Patch logs ###
 
1856
 
 
1857
import email
 
1858
import email.Parser
 
1859
import email.Generator
 
1860
 
 
1861
public('Patchlog')
 
1862
 
 
1863
class Patchlog(object):
 
1864
 
 
1865
    """Log entry associated to a revision.
 
1866
 
 
1867
    May be produced by `Revision.patchlog` or `ArchSourceTree.iter_logs()`. It
 
1868
    provides an extensive summary of the associated changeset, a natural
 
1869
    language description of the changes, and any number of user-defined
 
1870
    extension headers.
 
1871
 
 
1872
    Patchlogs are formatted according to RFC-822, and are parsed using the
 
1873
    standard email-handling facilities.
 
1874
 
 
1875
    Note that the patchlog text is not actually parsed before it is needed.
 
1876
    That deferred evaluation feature is implemented in the `_parse` method.
 
1877
 
 
1878
    The fundamental accessors are `__getitem__`, which give the text of a named
 
1879
    patchlog header, and the `description` property which gives the text of the
 
1880
    patchlog body, that is anything after the headers.
 
1881
 
 
1882
    Additional properties provide appropriate standard conversion of the
 
1883
    standard headers.
 
1884
    """
 
1885
 
 
1886
    def __init__(self, revision, tree=None, fromlib=False):
 
1887
        """Patchlog associated to the given revision.
 
1888
 
 
1889
        The patchlog may be retrieved from the provided ``tree``, from the
 
1890
        revision library if ``fromlib`` is set, or from the archive.
 
1891
 
 
1892
        :param tree: source tree to retrieve the patchlog from.
 
1893
        :type tree: `ArchSourceTree`, None
 
1894
        :param fromlib: retrieve the patchlog from the revision library.
 
1895
        :type fromblib: bool
 
1896
        """
 
1897
        if isinstance(revision, Revision):
 
1898
            self.__revision = revision.fullname
 
1899
        elif isinstance(revision, _unsafe):
 
1900
            self.__revision, = revision
 
1901
        else:
 
1902
            p = NameParser(revision)
 
1903
            if not p.has_archive() or not p.has_patchlevel:
 
1904
                raise errors.NamespaceError(revision,
 
1905
                                            'fully-qualified revision')
 
1906
            self.__revision = revision
 
1907
        self.tree = None
 
1908
        if tree is not None: self.tree = str(tree)
 
1909
        self.fromlib = bool(fromlib)
 
1910
        self.__email = None
 
1911
 
 
1912
    def __repr__(self):
 
1913
        if self.tree is not None:
 
1914
            return 'Patchlog(%s)' % repr(self.__revision)
 
1915
        else:
 
1916
            return 'Patchlog(%s, tree=%s)' % (repr(self.__revision),
 
1917
                                              repr(self.tree))
 
1918
 
 
1919
    def _parse(self):
 
1920
        """Deferred parsing of the log text."""
 
1921
        if self.__email is not None:
 
1922
            return self.__email
 
1923
        if self.tree is not None:
 
1924
            s = _arch.cat_log(self.tree, self.__revision)
 
1925
        elif self.fromlib:
 
1926
            s = _arch.library_log(self.__revision)
 
1927
        else:
 
1928
            s = _arch.cat_archive_log(self.__revision)
 
1929
        self.__email = email.Parser.Parser().parsestr(s)
 
1930
        return self.__email
 
1931
 
 
1932
    def __getitem__(self, header):
 
1933
        """Text of a patchlog header by name.
 
1934
 
 
1935
        :param header: name of a patchlog header.
 
1936
        :type header: str
 
1937
        :return: text of the header, or None if the header is not present.
 
1938
        :rtype: str, None
 
1939
        """
 
1940
        return self._parse()[header]
 
1941
 
 
1942
    def items(self):
 
1943
        """List of 2-tuples containing all headers and values.
 
1944
 
 
1945
        :rtype: list of 2-tuple of str
 
1946
        """
 
1947
        return self._parse().items()
 
1948
 
 
1949
    def get_revision(self):
 
1950
        """Deprecated.
 
1951
 
 
1952
        Revision associated to this patchlog.
 
1953
 
 
1954
        :rtype: `Revision`
 
1955
        :see: `Patchlog.revision`
 
1956
        """
 
1957
        deprecated_callable(self.get_revision, (Patchlog, 'revision'))
 
1958
        return self.revision
 
1959
 
 
1960
    def _get_revision(self):
 
1961
        assert self.__revision == self['Archive']+'/'+self['Revision']
 
1962
        return Revision(self.__revision)
 
1963
 
 
1964
    revision = property(_get_revision, doc="""
 
1965
    Revision associated to this patchlog.
 
1966
 
 
1967
    :type: `Revision`
 
1968
    """)
 
1969
 
 
1970
    def get_summary(self):
 
1971
        """Deprecated.
 
1972
 
 
1973
        Patchlog summary, a one-line natural language description.
 
1974
 
 
1975
        :rtype: str
 
1976
        :see: `Patchlog.summary`
 
1977
        """
 
1978
        deprecated_callable(self.get_summary, (Patchlog, 'summary'))
 
1979
        return self.summary
 
1980
 
 
1981
    def _get_summary(self):
 
1982
        return self['Summary']
 
1983
 
 
1984
    summary = property(_get_summary, doc="""
 
1985
    Patchlog summary, a one-line natural language description.
 
1986
 
 
1987
    :type: str
 
1988
    """)
 
1989
 
 
1990
    def get_description(self):
 
1991
        """Deprecated.
 
1992
 
 
1993
        Patchlog body, a long natural language description.
 
1994
 
 
1995
        :rtype: str
 
1996
        :see: `Patchlog.description`
 
1997
        """
 
1998
        deprecated_callable(self.get_description, (Patchlog, 'description'))
 
1999
        return self.description
 
2000
 
 
2001
    def _get_description(self):
 
2002
        m = self._parse()
 
2003
        assert not m.is_multipart()
 
2004
        return m.get_payload()
 
2005
 
 
2006
    description = property(_get_description, doc="""
 
2007
    Patchlog body, a long natural language description.
 
2008
 
 
2009
    :type: str
 
2010
    """)
 
2011
 
 
2012
    def get_date(self):
 
2013
        """Deprecated.
 
2014
 
 
2015
        For the description of the local time tuple, see the
 
2016
        documentation of the `time` module.
 
2017
 
 
2018
        :rtype: local time tuple
 
2019
        :see: `Patchlog.date`
 
2020
        """
 
2021
        deprecated_callable(self.get_date, (Patchlog, 'date'))
 
2022
        return self.date
 
2023
 
 
2024
    def _get_date(self):
 
2025
        d = self['Standard-date']
 
2026
        from time import strptime
 
2027
        return strptime(d, '%Y-%m-%d %H:%M:%S %Z')
 
2028
 
 
2029
    date = property(_get_date, doc="""
 
2030
    Time of the associated revision.
 
2031
 
 
2032
    For the description of the local time tuple, see the documentation
 
2033
    of the `time` module.
 
2034
 
 
2035
    :type: local time tuple
 
2036
    """)
 
2037
 
 
2038
    def get_creator(self):
 
2039
        """Deprecated.
 
2040
 
 
2041
        User id of the the creator of the associated revision.
 
2042
 
 
2043
        :rtype: str
 
2044
        :see: `Patchlog.creator`
 
2045
        """
 
2046
        deprecated_callable(self.get_creator, (Patchlog, 'creator'))
 
2047
        return self.creator
 
2048
 
 
2049
    def _get_creator(self):
 
2050
        return self['Creator']
 
2051
 
 
2052
    creator = property(_get_creator, doc="""
 
2053
    User id of the the creator of the associated revision.
 
2054
 
 
2055
    :type: str
 
2056
    """)
 
2057
 
 
2058
    def get_continuation(self):
 
2059
        """Deprecated.
 
2060
 
 
2061
        Ancestor of tag revisions.
 
2062
        None for commit and import revisions.
 
2063
 
 
2064
        :rtype: `Revision`, None.
 
2065
        :see: `Patchlog.continuation_of`
 
2066
        """
 
2067
        deprecated_callable(
 
2068
            self.get_continuation, (Patchlog, 'continuation_of'))
 
2069
        return self.continuation_of
 
2070
 
 
2071
    def _get_continuation(self):
 
2072
        deprecated_callable(
 
2073
            (Patchlog, 'continuation'), (Patchlog, 'continuation_of'))
 
2074
        return self.continuation_of
 
2075
 
 
2076
    continuation = property(_get_continuation, doc="""
 
2077
    Deprecated.
 
2078
 
 
2079
    Ancestor of tag revisions.
 
2080
    None for commit and import revisions.
 
2081
 
 
2082
    :type: `Revision`, None.
 
2083
    :see: `Patchlog.continuation_of`
 
2084
    """)
 
2085
 
 
2086
    def _get_continuation_of(self):
 
2087
        c = self['Continuation-of']
 
2088
        if c is None:
 
2089
            return None
 
2090
        return Revision(c)
 
2091
 
 
2092
    continuation_of = property(_get_continuation_of, doc="""
 
2093
    Ancestor of tag revisions.
 
2094
    None for commit and import revisions.
 
2095
 
 
2096
    :type: `Revision`, None.
 
2097
    """)
 
2098
 
 
2099
    def new_merge_revision(self, revision_name):
 
2100
        """Helper to produce a correct output even on broken baz logs
 
2101
 
 
2102
        :param revision_name: The name of a revision to produce
 
2103
        :type revision_name: str
 
2104
        """
 
2105
        if revision_name == "!!!!!nothing-should-depend-on-this":
 
2106
            return self.revision
 
2107
        else:
 
2108
            return Revision(revision_name)
 
2109
 
 
2110
    def get_new_patches(self):
 
2111
        """Deprecated.
 
2112
 
 
2113
        New-patches header as an iterable of Revision.
 
2114
 
 
2115
        :rtype: iterable of `Revision`
 
2116
        :see: `Patchlog.new_patches`
 
2117
        """
 
2118
        deprecated_callable(
 
2119
            Patchlog.get_new_patches, (Patchlog, 'new_patches'))
 
2120
        return self.new_patches
 
2121
 
 
2122
    def _get_new_patches(self):
 
2123
        return map(self.new_merge_revision, self['New-patches'].split())
 
2124
 
 
2125
    new_patches = property(_get_new_patches, doc="""
 
2126
    New-patches header as an iterable of Revision.
 
2127
 
 
2128
    Patchlogs added in this revision.
 
2129
 
 
2130
    :type: iterable of `Revision`
 
2131
    """)
 
2132
 
 
2133
    def get_merged_patches(self):
 
2134
        """Deprecated.
 
2135
 
 
2136
        Revisions merged in this revision. That is the revisions
 
2137
        listed in the New-patches header except the revision of the
 
2138
        patchlog.
 
2139
 
 
2140
        :rtype: iterable of `Revision`
 
2141
        :see: `Patchlog.merged_patches`
 
2142
        """
 
2143
        deprecated_callable(
 
2144
            Patchlog.get_merged_patches, (Patchlog, 'merged_patches'))
 
2145
        return self.merged_patches
 
2146
 
 
2147
    def _get_merged_patches(self):
 
2148
        rvsn = self.revision.fullname
 
2149
        return filter(lambda(r): r.fullname != rvsn, self.new_patches)
 
2150
 
 
2151
    merged_patches = property(_get_merged_patches, doc="""
 
2152
    Revisions merged in this revision.
 
2153
 
 
2154
    That is the revisions listed in the New-patches header except the
 
2155
    revision of the patchlog.
 
2156
 
 
2157
    :type: iterable of `Revision`
 
2158
    """)
 
2159
 
 
2160
    def get_new_files(self):
 
2161
        """Deprecated.
 
2162
 
 
2163
        Source files added in the associated revision.
 
2164
 
 
2165
        :rtype: iterable of `FileName`
 
2166
        :see: `Patchlog.new_files`
 
2167
        """
 
2168
        deprecated_callable(self.get_new_files, (Patchlog, 'new_files'))
 
2169
        return self.new_files
 
2170
 
 
2171
    def _get_new_files(self):
 
2172
        s = self['New-files']
 
2173
        if s is None: return []
 
2174
        return [ FileName(name_unescape(f)) for f in s.split() ]
 
2175
 
 
2176
    new_files = property(_get_new_files, doc="""
 
2177
    Source files added in the associated revision.
 
2178
 
 
2179
    :type: iterable of `FileName`
 
2180
    """)
 
2181
 
 
2182
    def get_modified_files(self):
 
2183
        """Deprecated.
 
2184
 
 
2185
        Names of source files modified in the associated revision.
 
2186
 
 
2187
        :rtype: iterable of `FileName`
 
2188
        """
 
2189
        deprecated_callable(
 
2190
            self.get_modified_files, (Patchlog, 'modified_files'))
 
2191
        return self.modified_files
 
2192
 
 
2193
    def _get_modified_files(self):
 
2194
        s = self['Modified-files']
 
2195
        if s is None: return []
 
2196
        return [ FileName(name_unescape(f)) for f in s.split() ]
 
2197
 
 
2198
    modified_files = property(_get_modified_files, doc="""
 
2199
    Names of source files modified in the associated revision.
 
2200
 
 
2201
    :type: iterable of `FileName`
 
2202
    """)
 
2203
 
 
2204
    def get_renamed_files(self):
 
2205
        """Deprecated.
 
2206
 
 
2207
        Source files renames in the associated revision.
 
2208
 
 
2209
        Dictionnary whose keys are old names and whose values are the
 
2210
        corresponding new names. Explicit file ids are listed in
 
2211
        addition to their associated source file.
 
2212
 
 
2213
        :rtype: dict
 
2214
        """
 
2215
        deprecated_callable(
 
2216
            self.get_renamed_files, (Patchlog, 'renamed_files'))
 
2217
        return self.renamed_files
 
2218
 
 
2219
    def _get_renamed_files(self):
 
2220
        s = self['Renamed-files']
 
2221
        renames = {}
 
2222
        if s is None:
 
2223
            return renames
 
2224
        s = s.split()
 
2225
        for i in range(len(s)/2):
 
2226
            renames[s[i*2]] = s[i*2+1]
 
2227
        return renames
 
2228
 
 
2229
    renamed_files = property(_get_renamed_files, doc="""
 
2230
    Source files renames in the associated revision.
 
2231
 
 
2232
    Dictionnary whose keys are old names and whose values are the
 
2233
    corresponding new names. Explicit file ids are listed in
 
2234
    addition to their associated source file.
 
2235
 
 
2236
    :type: dict
 
2237
    """)
 
2238
 
 
2239
    def get_removed_files(self):
 
2240
        """Deprecated.
 
2241
 
 
2242
        Names of source files removed in the associated revision.
 
2243
 
 
2244
        :rtype: iterable of `FileName`
 
2245
        """
 
2246
        deprecated_callable(
 
2247
            self.get_removed_files, (Patchlog, 'removed_files'))
 
2248
        return self.removed_files
 
2249
 
 
2250
    def _get_removed_files(self):
 
2251
        s = self['Removed-files']
 
2252
        if s is None: return []
 
2253
        return [ FileName(name_unescape(f)) for f in s.split() ]
 
2254
 
 
2255
    removed_files = property(_get_removed_files, doc="""
 
2256
    Names of source files removed in the associated revision.
 
2257
 
 
2258
    :type: iterable of `FileName`
 
2259
    """)
 
2260
 
 
2261
 
 
2262
public('LogMessage')
 
2263
 
 
2264
class LogMessage(object):
 
2265
 
 
2266
    """Log message for use with commit, import or tag operations.
 
2267
 
 
2268
    This is the write-enabled counterpart of Patchlog. When creating a new
 
2269
    revision with import, commit or tag, a log message file can be used to
 
2270
    specify a long description and custom headers.
 
2271
 
 
2272
    Commit and import can use the default log file of the source tree, with a
 
2273
    special name. You can create the LogMessage object associated to the
 
2274
    default log file with the WorkingTree.log_message method.
 
2275
 
 
2276
    For integration with external tools, it is useful to be able to parse an
 
2277
    existing log file and write the parsed object back idempotently. We are
 
2278
    lucky since this idempotence is provided by the standard email.Parser and
 
2279
    email.Generator.
 
2280
    """
 
2281
 
 
2282
    def __init__(self, name):
 
2283
        self.__name = name
 
2284
        self.__email = None
 
2285
        self.__dirty = False
 
2286
 
 
2287
    def get_name(self): return self.__name
 
2288
    name = property(get_name)
 
2289
 
 
2290
    def load(self):
 
2291
        """Read the log message from disk."""
 
2292
        self.__email = email.Parser.Parser().parse(file(self.__name))
 
2293
        self.__dirty = False
 
2294
 
 
2295
    def save(self):
 
2296
        """Write the log message to disk."""
 
2297
        if self.__dirty:
 
2298
            f = file(self.__name, 'w')
 
2299
            email.Generator.Generator(f).flatten(self.__email)
 
2300
            self.__dirty = False
 
2301
 
 
2302
    def clear(self):
 
2303
        """Clear the in-memory log message.
 
2304
 
 
2305
        When creating a new log message file, this method must be used
 
2306
        first before setting the message parts. That should help early
 
2307
        detection of erroneous log file names.
 
2308
        """
 
2309
        self.__email = email.Message.Message()
 
2310
        self.__dirty = True
 
2311
 
 
2312
    def __getitem__(self, header):
 
2313
        """Text of a patchlog header by name."""
 
2314
        if self.__email is None: self.load()
 
2315
        return self.__email[header]
 
2316
 
 
2317
    def __setitem__(self, header, text):
 
2318
        """Set a patchlog header."""
 
2319
        if self.__email is None: self.load()
 
2320
        try:
 
2321
            self.__email.replace_header(header, text)
 
2322
        except KeyError:
 
2323
            self.__email.add_header(header, text)
 
2324
        self.__dirty = True
 
2325
 
 
2326
    def get_description(self):
 
2327
        """Body of the log message."""
 
2328
        if self.__email is None: self.load()
 
2329
        assert not self.__email.is_multipart()
 
2330
        return self.__email.get_payload()
 
2331
 
 
2332
    def set_description(self, s):
 
2333
        """Set the body of the log message."""
 
2334
        if self.__email is None: self.load()
 
2335
        self.__email.set_payload(s)
 
2336
        self.__dirty = True
 
2337
 
 
2338
    description = property(get_description, set_description)
 
2339
 
 
2340
 
 
2341
### Source trees ###
 
2342
 
 
2343
public(
 
2344
    'init_tree',
 
2345
    'in_source_tree',
 
2346
    'tree_root',
 
2347
    )
 
2348
 
 
2349
def init_tree(directory, version=None, nested=False):
 
2350
    """Initialize a new project tree.
 
2351
 
 
2352
    :param directory: directory to initialize as a source tree.
 
2353
    :type directory: str
 
2354
    :param version: if given, set the the ``tree-version`` and create an empty
 
2355
       log version.
 
2356
    :type version: `Version`
 
2357
    :param nested: if true, the command will succeed even if 'directory'
 
2358
        is already within a source tree.
 
2359
    :type nested: bool
 
2360
    :return: source tree object for the given directory.
 
2361
    :rtype: `WorkingTree`
 
2362
    """
 
2363
    if version is not None:
 
2364
        version = _version_param(version)
 
2365
    _arch.init_tree(directory, version, nested)
 
2366
    return WorkingTree(directory)
 
2367
 
 
2368
 
 
2369
def in_source_tree(directory=None):
 
2370
    """Is directory inside a Arch source tree?
 
2371
 
 
2372
    :param directory: test if that directory is in an Arch source tree.
 
2373
    :type directory: str
 
2374
    :return: whether this directory is inside an Arch source tree.
 
2375
    :rtype: bool
 
2376
 
 
2377
    :warning: omitting the ``directory`` argument is deprecated.
 
2378
    """
 
2379
    if directory is None:
 
2380
        deprecated_usage(
 
2381
            in_source_tree, "Argument defaults to current direcotry.")
 
2382
        directory = '.'
 
2383
    return _arch.in_tree(directory)
 
2384
 
 
2385
 
 
2386
def tree_root(directory=None):
 
2387
    """SourceTree containing the given directory.
 
2388
 
 
2389
    :param directory: give the ``tree-root`` of this directory. Specify "." to
 
2390
        get the ``tree-root`` of the current directory.
 
2391
    :type directory: str
 
2392
    :return: source tree containing ``directory``.
 
2393
    :rtype: `ArchSourceTree`
 
2394
 
 
2395
    :warning: omitting the ``directory`` argument is deprecated.
 
2396
    """
 
2397
    if directory is None:
 
2398
        deprecated_usage(tree_root, "Argument defaults to current directory.")
 
2399
        directory = '.'
 
2400
    root = _arch.tree_root(directory)
 
2401
    # XXX use SourceTree to instanciate either WorkingTree or LibraryTree
 
2402
    return SourceTree(root)
 
2403
 
 
2404
 
 
2405
public('SourceTree',
 
2406
       'ForeignTree',
 
2407
       'ArchSourceTree',
 
2408
       'LibraryTree',
 
2409
       'WorkingTree')
 
2410
 
 
2411
class SourceTree(DirName):
 
2412
 
 
2413
    """Abstract base class for `ForeignTree` and `ArchSourceTree`."""
 
2414
 
 
2415
    def __new__(cls, root=None):
 
2416
        """Create a source tree object for the given root path.
 
2417
 
 
2418
        `ForeignTree` if root does not point to a Arch source tree.
 
2419
        `LibraryTree` if root is a tree in the revision library. `WorkingTree`
 
2420
        if root is a Arch source tree outside of the revision library.
 
2421
 
 
2422
        If root is omitted, use the tree-root of the current working directory.
 
2423
        """
 
2424
        __pychecker__ = 'no-returnvalues'
 
2425
        # checking root actually is a root done by LibraryTree and WorkingTree
 
2426
        if cls is not SourceTree:
 
2427
            assert root is not None
 
2428
            return DirName.__new__(cls, root)
 
2429
        else:
 
2430
            if not root:
 
2431
                root = _arch.tree_root(os.getcwd())
 
2432
            root = DirName(root).realpath()
 
2433
            if _arch.is_tree_root(root):
 
2434
                for revlib in _arch.iter_revision_libraries():
 
2435
                    if root.startswith(revlib):
 
2436
                      return LibraryTree(root)
 
2437
                return WorkingTree(root)
 
2438
            else:
 
2439
                assert not _arch.in_tree(root)
 
2440
                return ForeignTree(root)
 
2441
 
 
2442
 
 
2443
class ForeignTree(SourceTree):
 
2444
    """Generic source tree without Arch support.
 
2445
 
 
2446
    Unlike Arch source trees, the root may not even exist.
 
2447
    """
 
2448
    def __init__(self, root): pass
 
2449
 
 
2450
 
 
2451
class ArchSourceTree(SourceTree):
 
2452
 
 
2453
    """Abstract base class for Arch source trees."""
 
2454
 
 
2455
    def get_tree_version(self):
 
2456
        """Default version of the tree, also called tree-version.
 
2457
 
 
2458
        :raise errors.TreeVersionError: no valid default version set.
 
2459
        :raise IOError: unable to read the ++default-version file.
 
2460
        """
 
2461
        data = _arch.tree_version(self)
 
2462
        if data is None:
 
2463
            raise errors.TreeVersionError(self)
 
2464
        try:
 
2465
            return Version(data)
 
2466
        except errors.NamespaceError:
 
2467
            raise errors.TreeVersionError(self, data)
 
2468
    tree_version = property(get_tree_version)
 
2469
 
 
2470
    def _get_tree_revision(self):
 
2471
        """Revision of the last patchlog for the tree-version.
 
2472
 
 
2473
        :raise errors.TreeVersionError: no valid default version set.
 
2474
        :raise IOError: unable to read the ++default-version file.
 
2475
        """
 
2476
        reverse_logs = self.iter_logs(reverse=True)
 
2477
        try: last_log = reverse_logs.next()
 
2478
        except StopIteration:
 
2479
            raise RuntimeError('no logs for the tree-version')
 
2480
        return last_log.revision
 
2481
    tree_revision = property(_get_tree_revision)
 
2482
 
 
2483
    def check_is_tree_root(self):
 
2484
        assert os.path.isdir(self)
 
2485
        if not _arch.is_tree_root(self):
 
2486
            raise Exception, "%s is not a tla project tree" % self
 
2487
 
 
2488
    def get_tagging_method(self):
 
2489
        return _arch.tagging_method(self)
 
2490
    tagging_method = property(get_tagging_method)
 
2491
 
 
2492
    def __inventory_filter(self, trees, directories, files, both):
 
2493
        assert sum(map(bool, (trees, directories, files, both))) == 1
 
2494
        if trees: return lambda(t): SourceTree(self/name_unescape(t))
 
2495
        elif directories: return lambda(d): DirName(name_unescape(d))
 
2496
        elif files: return lambda(f): FileName(name_unescape(f))
 
2497
        elif both:
 
2498
            def ctor(f):
 
2499
                if os.path.isdir(self/f): return DirName(name_unescape(f))
 
2500
                else: return FileName(name_unescape(f))
 
2501
            return ctor
 
2502
        else:
 
2503
            raise AssertionError, 'not reached'
 
2504
 
 
2505
    def iter_inventory(self, source=False, precious=False, backups=False,
 
2506
                       junk=False, unrecognized=False, trees=False,
 
2507
                       directories=False, files=False, both=False,
 
2508
                       names=False, limit=None):
 
2509
        """Tree inventory.
 
2510
 
 
2511
        The kind of files looked for is specified by setting to
 
2512
        ``True`` exactly one of the following keyword arguments:
 
2513
 
 
2514
            ``source``, ``precious``, ``backups``, ``junk``,
 
2515
            ``unrecognized``, ``trees``.
 
2516
 
 
2517
        If the ``trees`` argument is not set, whether files, directory
 
2518
        or both should be listed is specified by setting to ``True``
 
2519
        exactly one of the following keyword arguments:
 
2520
 
 
2521
            ``directories``, ``files``, ``both``.
 
2522
 
 
2523
        :keyword source: list source files only.
 
2524
        :keyword precious: list precious files only.
 
2525
        :keyword backups: list backup files only.
 
2526
        :keyword junk: list junk files only.
 
2527
        :keyword unrecognized: list unrecognized files only.
 
2528
        :keyword trees: list nested trees only. If this is true, the
 
2529
            iterator wil only yield `ArchSourceTree` objects.
 
2530
 
 
2531
        :keyword directories: list directories only,
 
2532
            yield only `FileName` objects.
 
2533
        :keyword files: list files only, yield only `DirName` objects.
 
2534
        :keyword both: list both files and directories,
 
2535
            yield both FileName and `DirName` objects.
 
2536
 
 
2537
        :keyword name: do inventory as if the id-tagging-method was
 
2538
            set to ``names``. That is useful to ignore
 
2539
            ``untagged-source`` classification.
 
2540
 
 
2541
        :keyword limit: restrict the inventory to this directory. Must
 
2542
            be the name of a directory relative to the tree root.
 
2543
 
 
2544
        :rtype: iterator of `FileName`, `DirName`, `ArchSourceTree`
 
2545
            according to the arguments.
 
2546
        """
 
2547
        __pychecker__ = 'maxargs=12'
 
2548
        if limit is not None: _check_relname_param(limit, 'limit')
 
2549
        return itertools.imap\
 
2550
               (self.__inventory_filter(trees, directories, files, both),
 
2551
                _arch.iter_inventory(self, source, precious, backups,
 
2552
                                     junk, unrecognized, trees,
 
2553
                                     directories, files, both, names, limit))
 
2554
 
 
2555
    def iter_inventory_ids(self,source=False, precious=False, backups=False,
 
2556
                           junk=False, unrecognized=False, trees=False,
 
2557
                           directories=False, files=False, both=False,
 
2558
                           limit=None):
 
2559
        """Tree inventory with file ids.
 
2560
 
 
2561
        :see: `ArchSourceTree.iter_inventory`
 
2562
 
 
2563
        :rtype: iterator of tuples ``(id, item)``, where ``item`` is
 
2564
            `FileName`, `DirName`, `ArchSourceTree` according to the
 
2565
            arguments and ``id`` is the associated inventory id.
 
2566
        """
 
2567
        if limit is not None: _check_relname_param(limit, 'limit')
 
2568
        f = self.__inventory_filter(trees, directories, files, both)
 
2569
        def ff(t): name, id_ = t; return id_, f(name)
 
2570
        return itertools.imap\
 
2571
               (ff, _arch.iter_inventory_ids(self, source, precious, backups,
 
2572
                                             junk, unrecognized, trees,
 
2573
                                             directories, files, both, limit))
 
2574
 
 
2575
    def inventory(self, *args, **kw):
 
2576
        """Inventory of the source tree as a list. See iter_inventory.
 
2577
 
 
2578
        DEPRECATED
 
2579
        """
 
2580
        return list (self.iter_inventory(*args, **kw))
 
2581
 
 
2582
    def get_tree(self):
 
2583
        return util.sorttree(self.inventory(source=True, both=True))
 
2584
 
 
2585
    def iter_log_versions(self, limit=None, reverse=False):
 
2586
        """Iterate over versions this tree has a log-version for.
 
2587
 
 
2588
        :param limit: only iterate log-versions in this namespace.
 
2589
        :type limit: `Archive`, `Category`, `Branch`, `Version`
 
2590
        :param reverse: yield log-versions in reverse order.
 
2591
        :type reverse: bool
 
2592
        """
 
2593
        kwargs = {}
 
2594
        if isinstance(limit, Archive):
 
2595
            kwargs['archive'] = limit.name
 
2596
        elif isinstance(limit, (Category, Branch, Version)):
 
2597
            kwargs['archive'] = limit.archive.name
 
2598
        if isinstance(limit, Category): kwargs['category'] = limit.nonarch
 
2599
        elif isinstance(limit, Branch): kwargs['branch'] = limit.nonarch
 
2600
        elif isinstance(limit, Version): kwargs['version'] = limit.nonarch
 
2601
        elif limit is not None and not isinstance(limit, Archive):
 
2602
            raise TypeError("Expected Archive, Category, Branch or Version"
 
2603
                            " but got: %r" % limit)
 
2604
        for vsn in _arch.iter_log_versions(self, reverse=reverse, **kwargs):
 
2605
            yield Version(vsn)
 
2606
 
 
2607
    def iter_logs(self, version=None, reverse=False):
 
2608
        """Iterate over patchlogs present in this tree.
 
2609
 
 
2610
        :param version: list patchlogs from this version. Defaults to
 
2611
            the tree-version.
 
2612
        :type version: `Version`
 
2613
        :param reverse: iterate more recent logs first.
 
2614
        :type reverse: bool
 
2615
        :return: patchlogs from ``version``.
 
2616
        :rtype: iterator of `Patchlog`.
 
2617
        :raise errors.TreeVersionError: no valid default version set.
 
2618
        :raise IOError: unable to read the ++default-version file.
 
2619
        """
 
2620
        if version is None: version = self.tree_version.fullname
 
2621
        else: version = _version_param(version)
 
2622
        for rvsn in _arch.iter_log_ls(self, version, reverse=reverse):
 
2623
            yield Patchlog(_unsafe((rvsn,)), tree=self)
 
2624
 
 
2625
    def get_tag(self, file):
 
2626
        """File id set by an explicit id tag or tagline.
 
2627
 
 
2628
        :param file: name of a source file relative to the tree-root.
 
2629
        :type file: str
 
2630
        :return: file id if the file has an explicit id or a tagline.
 
2631
        :rtype: str
 
2632
        """
 
2633
        if not os.path.exists(self/file) and not os.path.islink(self/file):
 
2634
            raise IOError(2, 'No such file or directory', file)
 
2635
        return _arch.get_tag(self/file)
 
2636
 
 
2637
    def file_find(self, name, revision):
 
2638
         """Find a pristine copy of a file for a given revision.
 
2639
 
 
2640
         Will create the requested revision in the library or in the
 
2641
         current tree pristines if needed.
 
2642
 
 
2643
         :param name: name of the file.
 
2644
         :type name: str
 
2645
         :param revision: revision to look for the file into.
 
2646
         :type revision: `Revision`
 
2647
         :return: absolute path name of a pristine copy of the file.
 
2648
         :rtype: `pathname.PathName`
 
2649
         :raise errors.MissingFileError: file is not source or is not
 
2650
             present in the specified revision.
 
2651
         """
 
2652
         revision = _revision_param(revision)
 
2653
         result = _arch.file_find(self, name, revision)
 
2654
         if result is None:
 
2655
             raise errors.MissingFileError(self, name, revision)
 
2656
         # XXX Work around relative path when using pristines
 
2657
         result = os.path.join(self, result)
 
2658
         return PathName(result)
 
2659
 
 
2660
 
 
2661
class LibraryTree(ArchSourceTree):
 
2662
 
 
2663
    """Read-only Arch source tree."""
 
2664
 
 
2665
    def __init__(self, root):
 
2666
        """Create a LibraryTree object with the given root path.
 
2667
 
 
2668
        Root must be a directory containing a Arch source tree in the
 
2669
        revision library.
 
2670
        """
 
2671
        ArchSourceTree.__init__(self, root)
 
2672
        self.check_is_tree_root()
 
2673
 
 
2674
 
 
2675
class WorkingTree(ArchSourceTree):
 
2676
 
 
2677
    """Working source tree, Arch source tree which can be modified."""
 
2678
 
 
2679
    def __init__(self, root):
 
2680
        """Create WorkingTree object with the given root path.
 
2681
 
 
2682
        Root must be a directory containing a valid Arch source tree
 
2683
        outside of the revision library.
 
2684
        """
 
2685
        ArchSourceTree.__init__(self, root)
 
2686
        self.check_is_tree_root()
 
2687
 
 
2688
    def sync_tree(self, revision):
 
2689
        """Adds the patchlogs in the given revision to the current tree.
 
2690
 
 
2691
        Create a temporary source tree for ``revision``, then add all the
 
2692
        patchlogs present in that tree to the current tree. No content
 
2693
        is touched besides the patchlogs.
 
2694
 
 
2695
        :param revision: revision to synchronize with.
 
2696
        :type revision: `Version`, `Revision` or str
 
2697
        :raise errors.NamespaceError: ``revision`` is not a valid
 
2698
            version or revision name.
 
2699
        """
 
2700
        revision = _version_revision_param(revision)
 
2701
        _arch.sync_tree(self, revision)
 
2702
 
 
2703
    def set_tree_version(self, version):
 
2704
        version = _version_param(version)
 
2705
        _arch.set_tree_version(self, version)
 
2706
    tree_version = property(ArchSourceTree.get_tree_version, set_tree_version)
 
2707
 
 
2708
    def has_changes(self):
 
2709
        """Are there uncommited changes is this source tree?
 
2710
 
 
2711
        :rtype: bool
 
2712
        """
 
2713
        return _arch.has_changes(self)
 
2714
 
 
2715
    def changes(self, revision=None, output=None):
 
2716
        """Uncommited changes in this tree.
 
2717
 
 
2718
        :param revision: produce the changeset between this revision
 
2719
            and the tree. If ``revision`` is ``None``, use the
 
2720
            tree-revision.
 
2721
        :type revision: `Revision`, None
 
2722
        :param output: absolute path of the changeset to produce.
 
2723
        :type output: string
 
2724
        :return: changeset between ``revision`` and the tree.
 
2725
        :rtype: Changeset
 
2726
        :raise errors.TreeVersionError: no valid default version set.
 
2727
        :raise IOError: unable to read the ++default-version file.
 
2728
        """
 
2729
        if revision is None: revision = self.tree_revision
 
2730
        _check_str_param(output, 'output')
 
2731
        _check_revision_param(revision, 'revision')
 
2732
        return delta(revision, self, output)
 
2733
 
 
2734
 
 
2735
    def star_merge(self, from_=None, reference=None, forward=False,
 
2736
                   diff3=False):
 
2737
        """Merge mutually merged branches.
 
2738
 
 
2739
        :param from_: branch to merge changes from, ``None`` means the
 
2740
            ``tree-version``.
 
2741
        :type from_: None, `Version`, `Revision`, or str
 
2742
        :param reference: reference version for the merge, ``None``
 
2743
            means the ``tree-version``.
 
2744
        :type reference: None, `Version` or str
 
2745
        :param forward: ignore already applied patch hunks.
 
2746
        :type forward: bool
 
2747
        :param diff3: produce inline conflict markers instead of
 
2748
            ``.rej`` files.
 
2749
        :type diff3: bool
 
2750
        :raise errors.NamespaceError: ``from_`` or ``reference`` is
 
2751
            not a valid version or revision name.
 
2752
        """
 
2753
        list(self.iter_star_merge(from_, reference, forward, diff3))
 
2754
 
 
2755
    def iter_star_merge(self, from_=None, reference=None,
 
2756
                   forward=False, diff3=False):
 
2757
        """Merge mutually merged branches.
 
2758
 
 
2759
        :bug: if the merge causes a conflict, a RuntimeError is
 
2760
            raised. You should not rely on this behaviour as it is
 
2761
            likely to change in the future. If you want to support
 
2762
            conflicting merges, use `iter_star_merge` instead.
 
2763
 
 
2764
        :param `from_`: branch to merge changes from, ``None`` means the
 
2765
            ``tree-version``.
 
2766
        :type `from_`: None, `Version`, `Revision`, or str
 
2767
        :param reference: reference version for the merge, ``None``
 
2768
            means the ``tree-version``.
 
2769
        :type reference: None, `Version` or str
 
2770
        :param forward: ignore already applied patch hunks.
 
2771
        :type forward: bool
 
2772
        :param diff3: produce inline conflict markers instead of
 
2773
            ``.rej`` files.
 
2774
        :type diff3: bool
 
2775
        :raise errors.NamespaceError: ``from_`` or ``reference`` is
 
2776
            not a valid version or revision name.
 
2777
        :return: `MergeOutcome` iterator
 
2778
        :rtype: `iter_changes`
 
2779
        """
 
2780
        if from_ is not None:
 
2781
            from_ = _version_revision_param(from_)
 
2782
        if reference is not None:
 
2783
            reference = _version_revision_param(reference)
 
2784
        _arch.star_merge(self, from_, reference, forward, diff3)
 
2785
 
 
2786
    def iter_star_merge(self, from_=None, reference=None,
 
2787
                        forward=False, diff3=False):
 
2788
        """Merge mutually merged branches.
 
2789
 
 
2790
        :param `from_`: branch to merge changes from, ``None`` means the
 
2791
            ``tree-version``.
 
2792
        :type `from_`: None, `Version`, `Revision`, or str
 
2793
        :param reference: reference version for the merge, ``None``
 
2794
            means the ``tree-version``.
 
2795
        :type reference: None, `Version` or str
 
2796
        :param forward: ignore already applied patch hunks.
 
2797
        :type forward: bool
 
2798
        :param diff3: produce inline conflict markers instead of
 
2799
            ``.rej`` files.
 
2800
        :type diff3: bool
 
2801
        :raise errors.NamespaceError: ``from_`` or ``reference`` is
 
2802
            not a valid version or revision name.
 
2803
        :rtype: `ChangesetApplication`
 
2804
        """
 
2805
        # the --changes option must be supported by another method
 
2806
        # which returns a Changeset.
 
2807
        if from_ is not None:
 
2808
            from_ = _version_revision_param(from_)
 
2809
        if reference is not None:
 
2810
            reference = _version_revision_param(reference)
 
2811
        return ChangesetApplication(
 
2812
            _arch.iter_star_merge(self, from_, reference, forward, diff3))
 
2813
 
 
2814
    def undo(self, revision=None, output=None, quiet=False, throw_away=False):
 
2815
        """Undo and save changes in a project tree.
 
2816
 
 
2817
        Remove local changes since revision and optionally save them
 
2818
        as a changeset.
 
2819
 
 
2820
        :keyword revision: revision to revert to. Default to the last
 
2821
            revision of the tree-version for which a patchlog is present.
 
2822
        :type revision: `Revision`, str
 
2823
        :keyword output: name of the output changeset directory. Must
 
2824
            not already exist. Default to an automatic ,,undo-N name
 
2825
            in the working tree.
 
2826
        :type output: str
 
2827
        :keyword quiet: OBSOLETE. Incremental output is always discarded.
 
2828
        :type quiet: bool
 
2829
        :keyword throw_away: discard the output changeset and return
 
2830
            ``None``. Must not be used at the same time as ``output``.
 
2831
        :type throw_away: bool
 
2832
        :return: changeset restoring the undone changes,
 
2833
            or None if ``throw_away``.
 
2834
        :rtype: `Changeset`, None
 
2835
        """
 
2836
        assert sum(map(bool, (output, throw_away))) < 2
 
2837
        if output is None:
 
2838
            output = util.new_numbered_name(self, ',,undo-')
 
2839
        if revision is not None:
 
2840
            revision = _revision_param(revision)
 
2841
        _arch.undo(self, revision, output, quiet, throw_away)
 
2842
        if throw_away: return None
 
2843
        return Changeset(output)
 
2844
 
 
2845
    def redo(self, patch=None, keep=False, quiet=False):
 
2846
        """Redo changes in a project tree.
 
2847
 
 
2848
        Apply patch to the project tree and delete patch.
 
2849
 
 
2850
        If patch is provided, it must be a Changeset object. Else, the highest
 
2851
        numbered ,,undo-N directory in the project tree root is used.
 
2852
 
 
2853
        If keep is true, the patch directory is not deleted.
 
2854
        """
 
2855
        _arch.redo(self, patch, keep, quiet)
 
2856
 
 
2857
    def set_tagging_method(self, method):
 
2858
        _arch.set_tagging_method(self, method)
 
2859
    tagging_method = property(ArchSourceTree.get_tagging_method,
 
2860
                              set_tagging_method)
 
2861
 
 
2862
    def add_tag(self, file):
 
2863
        _arch.add(self/file)
 
2864
 
 
2865
    def move_tag(self, src, dest):
 
2866
        _arch.move(self/src, self/dest)
 
2867
 
 
2868
    def move_file(self, src, dest):
 
2869
        # FIXME: move explicit tag if present
 
2870
        dest = PathName(dest)
 
2871
        assert os.path.exists((self/dest).dirname())
 
2872
        os.rename(self/src, self/dest)
 
2873
 
 
2874
    def delete(self, file):
 
2875
        fullfile = self/file
 
2876
        if os.path.isfile(fullfile):
 
2877
            if _arch.has_explicit_id(fullfile):
 
2878
                _arch.delete(fullfile)
 
2879
            os.unlink(fullfile)
 
2880
        elif os.path.isdir(fullfile):
 
2881
            shutil.rmtree(fullfile)
 
2882
 
 
2883
    def del_file(self, file):
 
2884
        fullfile = self/file
 
2885
        if os.path.isfile(fullfile):
 
2886
            os.unlink(fullfile)
 
2887
        else:
 
2888
            shutil.rmtree(fullfile)
 
2889
 
 
2890
    def del_tag(self, file):
 
2891
        if not os.path.islink(self/file):
 
2892
            assert os.path.exists(self/file)
 
2893
        _arch.delete(self/file)
 
2894
 
 
2895
    def import_(self, log=None):
 
2896
        """Archive a full-source base-0 revision.
 
2897
 
 
2898
        If log is specified, it must be a LogMessage object or a file
 
2899
        name as a string. If omitted, the default log message file of
 
2900
        the tree is used.
 
2901
 
 
2902
        The --summary, --log-message and --setup options to tla are
 
2903
        mere CLI convenience features and are not directly supported.
 
2904
        """
 
2905
        if isinstance(log, LogMessage):
 
2906
            log.save()
 
2907
            log = log.name
 
2908
        assert log is None or isinstance(log, str)
 
2909
        _arch.import_(self, log)
 
2910
 
 
2911
    def commit(self, log=None, strict=False, seal=False, fix=False,
 
2912
               out_of_date_ok=False, file_list=None, version=None):
 
2913
        """Archive a changeset-based revision.
 
2914
 
 
2915
        :keyword version: version in which to commit the revision.
 
2916
            Defaults to the `tree_version`.
 
2917
        :type version: `Version`, str
 
2918
        :keyword log: Log message for this revision. Defaults to the log
 
2919
            message file of the tree-version (the file created by
 
2920
            ``tla make-log``.
 
2921
        :type log: `LogMessage`
 
2922
        :keyword strict: perform a strict tree-lint before commiting.
 
2923
        :type strict: bool
 
2924
        :keyword seal: create a ``version-0`` revision.
 
2925
        :type seal: bool
 
2926
        :keyword fix: create a ``versionfix`` revision.
 
2927
        :type fix: bool
 
2928
        :keyword out_of_date_ok: commit even if the tree is out of
 
2929
            date.
 
2930
        :type out_of_date_ok: bool
 
2931
        :keyword file_list: Only commit changes to those files,
 
2932
            specified relative to the tree-root.
 
2933
        :type file_list: iterable of str, with at least one item.
 
2934
 
 
2935
        The --summary and --log-message options to tla are mere CLI
 
2936
        convenience features and are not directly supported.
 
2937
 
 
2938
        :see: `WorkingTree.iter_commit`
 
2939
        """
 
2940
        for unused in self.iter_commit(
 
2941
            log, strict, seal, fix, out_of_date_ok, file_list, version):
 
2942
            pass
 
2943
 
 
2944
    def log_for_merge(self):
 
2945
        """Standard arch log of newly merged patches.
 
2946
 
 
2947
        :rtype: str
 
2948
        """
 
2949
        return _arch.log_for_merge(self)
 
2950
 
 
2951
 
 
2952
    def iter_commit(self, log=None, strict=False, seal=False, fix=False,
 
2953
                    out_of_date_ok=False, file_list=None, version=None,
 
2954
                    base=None):
 
2955
        """Archive a changeset-based revision, returning an iterator.
 
2956
 
 
2957
 
 
2958
        :keyword version: version in which to commit the revision.
 
2959
            Defaults to the `tree_version`.
 
2960
        :type version: `Version`, str
 
2961
        :keyword log: Log message for this revision. Defaults to the log
 
2962
            message file of the tree-version (the file created by
 
2963
            ``tla make-log``.
 
2964
        :type log: `LogMessage`
 
2965
        :keyword strict: perform a strict tree-lint before commiting.
 
2966
        :type strict: bool
 
2967
        :keyword seal: create a ``version-0`` revision.
 
2968
        :type seal: bool
 
2969
        :keyword fix: create a ``versionfix`` revision.
 
2970
        :type fix: bool
 
2971
        :keyword out_of_date_ok: commit even if the tree is out of
 
2972
            date.
 
2973
        :type out_of_date_ok: bool
 
2974
        :keyword file_list: Only commit changes to those files,
 
2975
            specified relative to the tree-root.
 
2976
        :type file_list: iterable of str, with at least one item.
 
2977
        :rtype: iterator of `TreeChange`, `Chatter` or str
 
2978
 
 
2979
        The --summary and --log-message options to tla are mere CLI
 
2980
        convenience features and are not directly supported.
 
2981
 
 
2982
        :see: `WorkingTree.commit`
 
2983
        """
 
2984
        if version is None:
 
2985
            version = self.tree_version.fullname
 
2986
        else:
 
2987
            version = _version_param(version)
 
2988
        if isinstance(log, LogMessage):
 
2989
            log.save()
 
2990
            log = log.name
 
2991
        assert log is None or isinstance(log, str)
 
2992
        if log is not None:
 
2993
            log = os.path.abspath(log)
 
2994
        iterator = _arch.iter_commit(
 
2995
            self, version, log, strict, seal, fix, out_of_date_ok, file_list,
 
2996
            base)
 
2997
        return classify_changeset_creation(iterator)
 
2998
 
 
2999
    def log_message(self, create=True, version=None):
 
3000
        """Default log-message object used by import and commit.
 
3001
 
 
3002
        If `create` is False, and the standard log file does not already
 
3003
        exists, return None. If `create` is True, use ``tla make-log`` if
 
3004
        needed.
 
3005
        """
 
3006
        if version is not None:
 
3007
            version = str(version)
 
3008
        name = _arch.log_name(self, version)
 
3009
        if not os.path.exists(name):
 
3010
            if not create: return None
 
3011
            _arch.make_log(self, version)
 
3012
        return LogMessage(name)
 
3013
 
 
3014
    def add_log_version(self, version):
 
3015
        """Add a patch log version to the project tree."""
 
3016
        version = _version_param(version)
 
3017
        _arch.add_log_version(self, version)
 
3018
 
 
3019
    def remove_log_version(self, version):
 
3020
        """Remove a patch log version from the project tree."""
 
3021
        version = _version_param(version)
 
3022
        _arch.remove_log_version(self, version)
 
3023
 
 
3024
    def replay(self):
 
3025
        """Replay changesets into this working tree."""
 
3026
        _arch.replay(self)
 
3027
 
 
3028
    def update(self):
 
3029
        """Apply delta of new revisions in the archive.
 
3030
 
 
3031
        Apply delta(A,B) on this working tree, where A and B are both
 
3032
        revisions of the tree version, A is the latest whose patchlog
 
3033
        is present in the tree and B is the latest present in the
 
3034
        archive.
 
3035
        """
 
3036
        _arch.update(self)
 
3037
 
 
3038
    def iter_pristines(self):
 
3039
        """Pristines present in that source tree.
 
3040
 
 
3041
        :return: Revisions which have pristine trees in that source tree.
 
3042
        :rtype: iterable of `Revision`
 
3043
        """
 
3044
        for rvsn in _arch.iter_pristines(self):
 
3045
            yield Revision(rvsn)
 
3046
 
 
3047
    def add_pristine(self, revision):
 
3048
        """Ensure that the project tree has a particular pristine revision.
 
3049
 
 
3050
        :param revision: revision to add a pristine for.
 
3051
        :type revision: `Revision`
 
3052
        """
 
3053
        revision = _revision_param(revision)
 
3054
        _arch.add_pristine(self, revision)
 
3055
 
 
3056
    def find_pristine(self, revision):
 
3057
        """Path to a pristine tree for the given revision.
 
3058
 
 
3059
        :param revision: find a pristine for that revision.
 
3060
        :type revision: `Revision`
 
3061
        :rtype: `ArchSourceTree`
 
3062
        :raise errors.NoPristineFoundError: no pristine tree was found
 
3063
            for the given revision.
 
3064
        """
 
3065
        if revision not in self.iter_pristines():
 
3066
            raise errors.NoPristineFoundError(self, revision)
 
3067
        revision = _revision_param(revision)
 
3068
        return ArchSourceTree(_arch.find_pristine(self, revision))
 
3069
 
 
3070
 
 
3071
### Changesets ###
 
3072
 
 
3073
public('Changeset')
 
3074
 
 
3075
class Changeset(DirName):
 
3076
 
 
3077
    """Arch whole-tree changeset."""
 
3078
 
 
3079
    def __init__(self, name):
 
3080
        DirName.__init__(self, name)
 
3081
        self.__index = {}
 
3082
        self.__index_excl = {}
 
3083
        self.__metadata = {}
 
3084
        self.exclude_re = re.compile('^[AE]_')
 
3085
 
 
3086
    def get_index(self, name, all=False):
 
3087
        """Load and parse an index file from the changeset.
 
3088
 
 
3089
        Expectable indexes are:
 
3090
        mod-dirs mod-files orig-dirs orig-files (more?)
 
3091
        """
 
3092
        key = (name, all)
 
3093
        if name in self.__index: return self.__index[key]
 
3094
        if all:
 
3095
            not_exclude = lambda(id_): True
 
3096
        else:
 
3097
            not_exclude = lambda(id_): not self.exclude_re.match(id_)
 
3098
        retval = {}
 
3099
        fullname = self/(name + '-index')
 
3100
        if os.path.exists(fullname):
 
3101
            index_file = open(fullname)
 
3102
            try:
 
3103
                for line in index_file:
 
3104
                    n, id_ = map(lambda(s): s.strip(), line.split())
 
3105
                    if not_exclude(id_):
 
3106
                        retval[id_] = os.path.normpath(name_unescape(n))
 
3107
            finally:
 
3108
                index_file.close()
 
3109
        self.__index[key] = retval
 
3110
        return retval
 
3111
 
 
3112
    def _get_does_nothing(self):
 
3113
        """Is the changeset a no-op?"""
 
3114
        # TODO: checking that all indices are empty can yield false negatives
 
3115
        # rather we should check for the effective absence of changes.
 
3116
        for index_name in ('mod-files', 'orig-files', 'mod-dirs', 'orig-dirs'):
 
3117
            index = self.get_index(index_name, True)
 
3118
            if index: return False
 
3119
        return True
 
3120
    does_nothing = property(_get_does_nothing)
 
3121
 
 
3122
#     def get_metadata(self, name):
 
3123
#         """Load and parse a metadata file from the changeset.
 
3124
#
 
3125
#         Expectable metadata tables are:
 
3126
#         modified-only-dir original-only-dir (more?)
 
3127
#         """
 
3128
#         raise NotImplementedError, "not yet implemented"
 
3129
#         if name in self.__metadata: return self.__metadata[name]
 
3130
#         retval = []
 
3131
#         fullname = self/(name + '-metadata')
 
3132
#         if os.path.exists(fullname):
 
3133
#             for line in open(fullname):
 
3134
#                 pass # Not implemented
 
3135
#         self.__metadata[name] = retval
 
3136
#         return retval
 
3137
 
 
3138
    def __iter_mod_helper(self, what, all):
 
3139
        __pychecker__ = 'no-abstract' # for ImmutableSet
 
3140
        orig_index = self.get_index('orig-' + what, all)
 
3141
        mod_index = self.get_index('mod-' + what, all)
 
3142
        orig_ids = ImmutableSet(orig_index.iterkeys())
 
3143
        mod_ids = ImmutableSet(mod_index.iterkeys())
 
3144
        for id_ in orig_ids & mod_ids:
 
3145
            yield (id_, orig_index[id_], mod_index[id_])
 
3146
 
 
3147
    def iter_mod_files(self, all=False):
 
3148
        """Iterator over (id, orig, mod) tuples for files which are are
 
3149
        patched, renamed, or have their permissions changed."""
 
3150
        return self.__iter_mod_helper('files', all)
 
3151
 
 
3152
    def iter_patched_files(self, all=False):
 
3153
        """Iterate over (id, orig, mod) of patched files."""
 
3154
        patchdir = self/'patches'
 
3155
        for f in self.iter_mod_files(all):
 
3156
            if os.path.isfile(patchdir/f[1]+'.patch'):
 
3157
                yield f
 
3158
            else:
 
3159
                print "file does not exists: %s" % str(patchdir/f[1]+'.patch')
 
3160
        #return itertools.ifilter(lambda(f): os.path.isfile(patchdir/f[2]),
 
3161
        #                         self.iter_mod_files())
 
3162
 
 
3163
    def patch_file(self, modname):
 
3164
        return self/'patches'/modname+'.patch'
 
3165
 
 
3166
    def __iter_renames_helper(self, what):
 
3167
        for id_, orig, mod in self.__iter_mod_helper(what, all=False):
 
3168
            if orig != mod:
 
3169
                yield (id_, orig, mod)
 
3170
 
 
3171
    def iter_renames(self):
 
3172
        """Iterate over (id, orig, dest) triples representing renames.
 
3173
 
 
3174
        id is the persistant file tag, and the key of the dictionnary.
 
3175
        orig is the name in the original tree.
 
3176
        dest is the name in the modified tree.
 
3177
        """
 
3178
        from itertools import chain
 
3179
        return chain(*map(self.__iter_renames_helper, ('files', 'dirs')))
 
3180
 
 
3181
    def __iter_created_helper(self, what, removed=False, all=False):
 
3182
        __pychecker__ = 'no-abstract' # for ImmutableSet
 
3183
        orig_index = self.get_index('orig-' + what, all)
 
3184
        mod_index = self.get_index('mod-' + what, all)
 
3185
        if removed: orig_index, mod_index = mod_index, orig_index
 
3186
        orig_ids = ImmutableSet(orig_index.iterkeys())
 
3187
        mod_ids = ImmutableSet(mod_index.iterkeys())
 
3188
        for id_ in mod_ids - orig_ids:
 
3189
            yield (id_, mod_index[id_])
 
3190
 
 
3191
    def iter_created_files(self, all=False):
 
3192
        """Iterator over tuples (id, dest) for created files.
 
3193
 
 
3194
        :param all: include Arch control files.
 
3195
        :type all: bool
 
3196
        """
 
3197
        return self.__iter_created_helper('files', all=all)
 
3198
 
 
3199
    def created_file(self, name):
 
3200
        return self/'new-files-archive'/name
 
3201
 
 
3202
    def iter_removed_files(self, all=False):
 
3203
        """Iterator over tuples (id, orig) for removed files.
 
3204
 
 
3205
        :param all: include Arch control files.
 
3206
        :type all: bool
 
3207
        """
 
3208
        return self.__iter_created_helper('files', removed=True, all=all)
 
3209
 
 
3210
    def removed_file(self, name):
 
3211
        return self/'removed-files-archive'/name
 
3212
 
 
3213
    def iter_created_dirs(self, all=False):
 
3214
        """Iterator over tuples (id, dest) for created directories.
 
3215
 
 
3216
        :param all: include Arch control files.
 
3217
        :type all: bool
 
3218
        """
 
3219
        return self.__iter_created_helper('dirs', all=all)
 
3220
 
 
3221
    def iter_removed_dirs(self, all=False):
 
3222
        """Iterator over tuples (id, orig) for removed directories.
 
3223
 
 
3224
        :param all: include Arch control files.
 
3225
        :type all: bool
 
3226
        """
 
3227
        return self.__iter_created_helper('dirs', removed=True, all=all)
 
3228
 
 
3229
    def iter_apply(self, tree, reverse=False):
 
3230
        """Apply this changeset to a tree, with incremental output.
 
3231
 
 
3232
        :param tree: the tree to apply changes to.
 
3233
        :type tree: `WorkingTree`
 
3234
        :param reverse: invert the meaning of the changeset; adds
 
3235
            become deletes, etc.
 
3236
        :type reverse: bool
 
3237
        :rtype: `ChangesetApplication`
 
3238
        """
 
3239
        _check_working_tree_param(tree, 'tree')
 
3240
        return ChangesetApplication(
 
3241
            _arch.iter_apply_changeset(self, tree, reverse))
 
3242
 
 
3243
 
 
3244
public('changeset')
 
3245
 
 
3246
def changeset(orig, mod, dest):
 
3247
    """Deprecated.
 
3248
 
 
3249
    :see: `delta`
 
3250
    :rtype: `Changeset`
 
3251
    """
 
3252
    deprecated_callable(changeset, delta)
 
3253
    return delta(ArchSourceTree(orig), ArchSourceTree(mod), dest)
 
3254
 
 
3255
 
 
3256
def _tree_or_existing_revision_param(param, name):
 
3257
    if isinstance(param, Revision):
 
3258
        # should check for actual existence, but that is expensive
 
3259
        if not param.archive.is_registered():
 
3260
            raise errors.ArchiveNotRegistered(param.archive)
 
3261
        return param.fullname
 
3262
    elif isinstance(param, ArchSourceTree):
 
3263
        return param
 
3264
    else:
 
3265
        raise TypeError("Parameter \"%s\" must be ArchSourceTree or Revision"
 
3266
                        " but was: %r" % (name, param))
 
3267
 
 
3268
def _check_str_param(param, name):
 
3269
    """Internal argument checking utility"""
 
3270
    if not isinstance(param, basestring):
 
3271
        raise TypeError("Parameter \"%s\" must be a string"
 
3272
                        " but was: %r" % (name, param))
 
3273
 
 
3274
def _check_working_tree_param(param, name):
 
3275
    """Internal argument checking utility"""
 
3276
    if not isinstance(param, WorkingTree):
 
3277
        raise TypeError("Parameter \"%s\" must be a WorkingTree"
 
3278
                        " but was: %r" % (name, param))
 
3279
 
 
3280
 
 
3281
public('ChangesetApplication', 'ChangesetCreation', 'delta', 'iter_delta')
 
3282
 
 
3283
class ChangesetApplication(object):
 
3284
    """Incremental changeset application process."""
 
3285
 
 
3286
    def __init__(self, proc):
 
3287
        """For internal use only."""
 
3288
        self._iter_process = proc
 
3289
 
 
3290
    def __iter__(self):
 
3291
        """Return an iterator of `MergeOutcome`"""
 
3292
        return classify_changeset_application(self._iter_process)
 
3293
 
 
3294
    def _get_conflicting(self):
 
3295
        "Did conflicts occur during changeset application?"
 
3296
        if not self.finished:
 
3297
            raise AttributeError("Changeset application is not complete.")
 
3298
        return self._iter_process.status == 1
 
3299
    conflicting = property(_get_conflicting)
 
3300
 
 
3301
    def _get_finished(self):
 
3302
        "Is the changeset application complete?"
 
3303
        return self._iter_process.finished
 
3304
    finished = property(_get_finished)
 
3305
 
 
3306
class ChangesetCreation(object):
 
3307
    """Incremental changeset generation process."""
 
3308
 
 
3309
    def __init__(self, proc, dest):
 
3310
        """For internal use only."""
 
3311
        self._iter_process = proc
 
3312
        self._dest = dest
 
3313
        self._changeset = None
 
3314
 
 
3315
    def __iter__(self):
 
3316
        """Return an iterator of `TreeChange`"""
 
3317
        return classify_changeset_creation(self._iter_process)
 
3318
 
 
3319
    def _get_changeset(self):
 
3320
        """Generated changeset."""
 
3321
        if self._changeset is None:
 
3322
            if not self.finished:
 
3323
                raise AttributeError("Changeset generation is not complete.")
 
3324
            self._changeset = Changeset(self._dest)
 
3325
        return self._changeset
 
3326
    changeset = property(_get_changeset)
 
3327
 
 
3328
    def _get_finished(self):
 
3329
        "Is the changeset creation complete?"
 
3330
        return self._iter_process.finished
 
3331
    finished = property(_get_finished)
 
3332
 
 
3333
 
 
3334
def iter_delta(orig, mod, dest):
 
3335
    """Compute a whole-tree changeset with incremental output.
 
3336
 
 
3337
    :param orig: old revision or directory.
 
3338
    :type orig: `Revision`, `ArchSourceTree`
 
3339
    :param mod: new revision or directory,
 
3340
    :type mod: `Revision`, `ArchSourceTree`
 
3341
    :param dest: path of the changeset to create.
 
3342
    :type dest: str
 
3343
    :rtype: `ChangesetCreation`
 
3344
    """
 
3345
    orig = _tree_or_existing_revision_param(orig, 'orig')
 
3346
    mod = _tree_or_existing_revision_param(mod, 'mod')
 
3347
    _check_str_param(dest, 'dest')
 
3348
    proc = _arch.iter_delta(orig, mod, dest)
 
3349
    return ChangesetCreation(proc, dest)
 
3350
 
 
3351
 
 
3352
def delta(orig, mod, dest):
 
3353
    """Compute a whole-tree changeset.
 
3354
 
 
3355
    Create the output directory ``dest`` (it must not already exist).
 
3356
 
 
3357
    Compare the source trees ``orig`` and ``mod`` (which may be source
 
3358
    arch source tree or revisions). Create a changeset in ``dest``.
 
3359
 
 
3360
    :param orig: the old revision or directory.
 
3361
    :type orig: `Revision`, `ArchSourceTree`
 
3362
    :param mod: the new revision or directory.
 
3363
    :type mod: `Revision`, `ArchSourceTree`
 
3364
    :param dest: path of the changeset to create.
 
3365
    :type dest: str
 
3366
    :return: changeset from ``orig`` to ``mod``.
 
3367
    :rtype: `Changeset`
 
3368
    """
 
3369
    __pychecker__ = 'unusednames=line'
 
3370
    iter_ = iter_delta(orig, mod, dest)
 
3371
    for line in iter_: pass
 
3372
    return iter_.changeset
 
3373
 
 
3374
 
 
3375
### Top level archive functions ###
 
3376
 
 
3377
public('my_id', 'set_my_id', 'make_archive', 'register_archive')
 
3378
 
 
3379
def my_id():
 
3380
    """The current registered user id
 
3381
 
 
3382
    :return: the user id, for example 'John Doe <jdoe@example.org>'.
 
3383
    :rtype: str
 
3384
    """
 
3385
    return _arch.my_id()
 
3386
 
 
3387
 
 
3388
def set_my_id(new_id):
 
3389
    """Set the current registered user id
 
3390
 
 
3391
    :param new_id: new value of the user id.
 
3392
    :type new_id: str
 
3393
    """
 
3394
    return _arch.set_my_id(new_id)
 
3395
 
 
3396
 
 
3397
def make_archive(name, location, signed=False, listing=False):
 
3398
    """Create and register new commitable archive.
 
3399
 
 
3400
    :param name: archive name (e.g. "david@allouche.net--2003b").
 
3401
    :type name: `Archive` or str
 
3402
    :param location: URL of the archive
 
3403
    :type location: str
 
3404
    :param signed: create GPG signatures for the archive contents.
 
3405
    :type signed: bool
 
3406
    :param listing: maintains ''.listing'' files to enable HTTP access.
 
3407
    :type listing: bool
 
3408
 
 
3409
    :return: an `Archive` instance for the given name.
 
3410
    :rtype: `Archive`
 
3411
 
 
3412
    :raise errors.NamespaceError: ``name`` is not a valid archive name.
 
3413
    """
 
3414
    name = _archive_param(name)
 
3415
    _arch.make_archive(name, location, signed, listing)
 
3416
    return Archive(_unsafe((name,)))
 
3417
 
 
3418
def register_archive(name, location):
 
3419
    """Record the location of an archive.
 
3420
 
 
3421
    :param name: archive name, or None to use the official name stored in the
 
3422
        archive.
 
3423
    :type name: str, None
 
3424
    :param location: URL of the archive.
 
3425
    :type location: str
 
3426
    :return: newly registered archive.
 
3427
    :rtype: `Archive`.
 
3428
    """
 
3429
    if name is not None:
 
3430
        name = _archive_param(name)
 
3431
    _arch.register_archive(name, location)
 
3432
    if name is not None:
 
3433
        return Archive(_unsafe((name,)))
 
3434
    else:
 
3435
        for n in _arch.archives():
 
3436
            a = Archive(_unsafe((n,)))
 
3437
            if a.location == location:
 
3438
                return a
 
3439
        raise AssertionError, 'not reached'
 
3440
 
 
3441
 
 
3442
public('iter_archives', 'archives')
 
3443
 
 
3444
def iter_archives():
 
3445
    """Iterate over registered archives.
 
3446
 
 
3447
    :return: all registered archives.
 
3448
    :rtype: iterable of `Archive`
 
3449
    """
 
3450
    for n in _arch.archives():
 
3451
        yield Archive(n)
 
3452
 
 
3453
 
 
3454
def archives():
 
3455
    """Deprecated.
 
3456
 
 
3457
    List of registered archives.
 
3458
 
 
3459
    :rtype: sequence of `Archive`
 
3460
    :see: `iter_archives`
 
3461
    """
 
3462
    deprecated_callable(archives, iter_archives)
 
3463
    return list(iter_archives())
 
3464
 
 
3465
 
 
3466
public('iter_library_archives', 'library_archives')
 
3467
 
 
3468
def iter_library_archives():
 
3469
    """Iterate over archives present in the revision library.
 
3470
 
 
3471
    :returns: all archives which are present in the revision library.
 
3472
    :rtype: iterable of `Archive`
 
3473
    """
 
3474
    for n in _arch.library_archives():
 
3475
        yield Archive(n)
 
3476
 
 
3477
 
 
3478
def library_archives():
 
3479
    """Deprecated.
 
3480
 
 
3481
    List of archives present in the revision library.
 
3482
 
 
3483
    :rtype: sequence of `Archive`
 
3484
    :see: `iter_library_archives`
 
3485
    """
 
3486
    deprecated_callable(library_archives, iter_library_archives)
 
3487
    return list(iter_library_archives())
 
3488
 
 
3489
 
 
3490
public('default_archive')
 
3491
 
 
3492
def default_archive():
 
3493
    """Default Archive object or None.
 
3494
 
 
3495
    :return: the default archive, or None.
 
3496
    :rtype: `Archive`, None
 
3497
    """
 
3498
    name = _arch.default_archive()
 
3499
    if name is None:
 
3500
        return None
 
3501
    else:
 
3502
        return Archive(name)
 
3503
 
 
3504
 
 
3505
public('make_continuation')
 
3506
 
 
3507
def make_continuation(source_revision, tag_version):
 
3508
    """Deprecated.
 
3509
 
 
3510
    :see: `Revision.make_continuation`
 
3511
    """
 
3512
    deprecated_callable(make_continuation, Revision.make_continuation)
 
3513
    source_revision = _version_revision_param(source_revision)
 
3514
    tag_version = _version_param(tag_version)
 
3515
    _arch.archive_setup(tag_version)
 
3516
    _arch.tag(source_revision, tag_version)
 
3517
 
 
3518
public('get', 'get_patch')
 
3519
 
 
3520
def get(revision, dir, link=False):
 
3521
    """Deprecated.
 
3522
 
 
3523
    Construct a project tree for a revision.
 
3524
 
 
3525
    :rtype: `WorkingTree`
 
3526
    :see: `Revision.get`
 
3527
    """
 
3528
    deprecated_callable(get, Revision.get)
 
3529
    revision = _package_revision_param(revision)
 
3530
    _arch.get(revision, dir, link)
 
3531
    return WorkingTree(dir)
 
3532
 
 
3533
 
 
3534
def get_patch(revision, dir):
 
3535
    """Deprecated.
 
3536
 
 
3537
    :rtype: `Changeset`
 
3538
    :see: `Revision.get_patch`
 
3539
    """
 
3540
    deprecated_callable(get_patch, Revision.get_patch)
 
3541
    revision = _revision_param(revision)
 
3542
    _arch.get_patch(revision, dir)
 
3543
    return Changeset(dir)
 
3544
 
 
3545
 
 
3546
public(
 
3547
    'iter_revision_libraries',
 
3548
    'register_revision_library',
 
3549
    'unregister_revision_library',
 
3550
    )
 
3551
 
 
3552
def iter_revision_libraries():
 
3553
    """Iterate over registered revision library directories.
 
3554
 
 
3555
    :return: directory names of all registered revision libraries.
 
3556
    :rtype: iterable of str
 
3557
    """
 
3558
    return _arch.iter_revision_libraries()
 
3559
 
 
3560
def register_revision_library(dirname):
 
3561
    """Register an existing revision library directory.
 
3562
 
 
3563
    :param dirname: absolute path name of existing user-writable directory.
 
3564
    :type dirname: str
 
3565
    :todo: create_revision_library which abstracts out revlib construction.
 
3566
    :postcondition: ``dirname`` is present in `iter_revision_libraries` output.
 
3567
    """
 
3568
    if not os.path.isabs(dirname):
 
3569
        raise ValueError, "not an absolute path: %r" % dirname
 
3570
    if not os.path.isdir(dirname):
 
3571
        raise ValueError, "directory does not exist: %r" % dirname
 
3572
    _arch.register_revision_library(dirname)
 
3573
 
 
3574
def unregister_revision_library(dirname):
 
3575
    """Unregister a revision library directory.
 
3576
 
 
3577
    :param dirname: registered revision library directory.
 
3578
    :type dirname: str
 
3579
    :todo: delete_revision_library which abstracts out revlib destruction.
 
3580
    :precondition: ``dirname`` is present in `iter_revision_libraries` output.
 
3581
    :postcondition: ``dirname`` is not listed by `iter_revision_libraries`.
 
3582
    """
 
3583
    if not os.path.isabs(dirname):
 
3584
        raise ValueError, "not an absolute path: %r" % dirname
 
3585
    _arch.unregister_revision_library(dirname)
 
3586
 
 
3587
 
 
3588
### Parsing Arch names ###
 
3589
 
 
3590
public('NameParser')
 
3591
 
 
3592
class NameParser(_arch.NameParser):
 
3593
 
 
3594
    """Parser for names in Arch archive namespace.
 
3595
 
 
3596
    :see: `backends.tla.NameParser`
 
3597
    """
 
3598
 
 
3599
    def object(self):
 
3600
        """Create the Category, Branch, Version or Revision object
 
3601
 
 
3602
        Create the namespace object corresponding to the name. This
 
3603
        requires some guessing so, for example, nameless branches will
 
3604
        not be recognized.
 
3605
 
 
3606
        :warning: DEPRECATED: unsafe and not really useful.
 
3607
        """
 
3608
        a = self.get_archive()
 
3609
        if not a: return None
 
3610
        a = Archive(a)
 
3611
        c = self.get_category()
 
3612
        if not c: return a
 
3613
        c = a[c]
 
3614
        b = self.get_branch()
 
3615
        if not b: return c
 
3616
        b = c[b]
 
3617
        v = self.get_version()
 
3618
        if not v: return b
 
3619
        v = b[v]
 
3620
        r = self.get_patchlevel()
 
3621
        if not r: return v
 
3622
        return v[r]
 
3623
 
 
3624
 
 
3625
### Searching in Archives ###
 
3626
 
 
3627
public(
 
3628
    'filter_archive_logs',
 
3629
    'filter_revisions',
 
3630
    'grep_summary',
 
3631
    'grep_summary_interactive',
 
3632
    'suspected_move',
 
3633
    'revisions_merging',
 
3634
    'temphack',
 
3635
    'revision_which_created',
 
3636
    'last_revision',
 
3637
    'map_name_id',
 
3638
    )
 
3639
 
 
3640
def filter_archive_logs(limit, pred):
 
3641
    deprecated_callable(filter_archive_logs,
 
3642
                        because='It does not belong here.')
 
3643
    for r in limit.iter_revisions():
 
3644
        if pred(r.patchlog): yield r
 
3645
 
 
3646
 
 
3647
def filter_revisions(limit, pred):
 
3648
    deprecated_callable(filter_archive_logs,
 
3649
                        because='It does not belong here.')
 
3650
    for r in limit.iter_revisions():
 
3651
        if pred(r): yield r
 
3652
 
 
3653
def grep_summary(limit, rx):
 
3654
    deprecated_callable(grep_summary,
 
3655
                        because='It does not belong here.')
 
3656
    crx = re.compile(rx)
 
3657
    def pred(p): return crx.search(p['Summary'])
 
3658
    return filter_archive_logs(limit, pred)
 
3659
 
 
3660
 
 
3661
def grep_summary_interactive(limit):
 
3662
    deprecated_callable(grep_summary_interactive,
 
3663
                        because='It does not belong here.')
 
3664
    while True:
 
3665
        try:
 
3666
            rx = raw_input('search> ')
 
3667
        except KeyboardInterrupt:
 
3668
            break
 
3669
        if not rx: break
 
3670
        for r in grep_summary(limit, rx):
 
3671
            p = r.patchlog
 
3672
            print 'Revision:', p['Revision']
 
3673
            print 'Summary: ', p['Summary']
 
3674
 
 
3675
 
 
3676
def suspected_move(limit):
 
3677
    deprecated_callable(suspected_move,
 
3678
                        because='It does not belong here.')
 
3679
    def pred(p): return bool(p['New-files'] and p['Removed-files'])
 
3680
    return filter_archive_logs(limit, pred)
 
3681
 
 
3682
 
 
3683
def revisions_merging(limit, rev):
 
3684
    deprecated_callable(revisions_merging,
 
3685
                        because='It does not belong here.')
 
3686
    def pred(p): return rev in p.merged_patches
 
3687
    return filter_archive_logs(limit, pred)
 
3688
 
 
3689
 
 
3690
def temphack(revision):
 
3691
    deprecated_callable(temphack,
 
3692
                        because='It does not belong here.')
 
3693
    import sets
 
3694
    retval = sets.Set()
 
3695
    for ancstr in revision.iter_ancestors(metoo=True):
 
3696
        for k in ancstr.patchlog.keys():
 
3697
            retval.add(k)
 
3698
    return retval
 
3699
 
 
3700
 
 
3701
def revision_which_created(file, revision):
 
3702
    deprecated_callable(revision_which_created,
 
3703
                        because='It does not belong here.')
 
3704
    for ancstr in revision.iter_ancestors(metoo=True):
 
3705
        if file in ancstr.patchlog.new_files:
 
3706
            return ancstr
 
3707
    return None
 
3708
 
 
3709
def last_revision(tree):
 
3710
    deprecated_callable(last_revision, (ArchSourceTree, 'tree_revision'))
 
3711
    tree = SourceTree(tree)
 
3712
    return tree.iter_logs(reverse=True).next().revision
 
3713
 
 
3714
 
 
3715
def map_name_id(tree):
 
3716
    deprecated_callable(map_name_id,
 
3717
                        because='It does not belong here.')
 
3718
    if not isinstance(tree, SourceTree):
 
3719
        tree = SourceTree(tree)
 
3720
    retval = {}
 
3721
    for id_, name in tree.iter_inventory_ids(source=True, files=True):
 
3722
        retval[name] = id_
 
3723
    return retval
 
3724