~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/pylon/arch_compound.py

  • Committer: Robert Collins
  • Date: 2005-09-13 15:11:39 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-20050913151139-9ac920fc9d7bda31
TODOification

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004 Aaron Bentley
 
2
# <aaron.bentley@utoronto.ca>
 
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
import os
 
19
import shutil
 
20
import re
 
21
import tempfile
 
22
 
 
23
import util
 
24
import pybaz
 
25
import arch_core
 
26
import native
 
27
import errors
 
28
from arch_core import chattermatch
 
29
import misc
 
30
import patches
 
31
 
 
32
__docformat__ = "restructuredtext"
 
33
__doc__ = "Compound functionality built on lower-level commands"
 
34
 
 
35
def iter_any_ancestry(tree, startrev):
 
36
    return _iter_ancestry(tree, startrev)
 
37
 
 
38
def _iter_ancestry(tree, startrev):
 
39
    """Iterate over ancestry according to tree's patchlogs.
 
40
 
 
41
    Generator that yields `arch.Patchlog` or `arch.Revision` instances
 
42
    according to `tree` ancestry. Each item returned is the direct ancestor of
 
43
    the previous `arch.Revision`. If `startrev` is supplied, it is the first
 
44
    item yielded. The iteration stops after the first ancestor named in a
 
45
    patchlog that does not, itself, have a patchlog in `tree`, or at the
 
46
    beginning.
 
47
 
 
48
    :param tree: tree to determine the ancestry of
 
49
    :type tree: `arch.ArchSourceTree`
 
50
    :param startrev: revision to start at. Must be in the ancestry of `tree`.
 
51
    :type startrev: `arch.Revision`
 
52
    :rtype: iterator of `arch.Patchlog` or `arch.Revision`
 
53
    """
 
54
    log = None
 
55
    while startrev is not None:
 
56
        nextrev = None
 
57
        skip = True
 
58
        for log in tree.iter_logs(startrev.version, reverse=True):
 
59
            if skip and log.revision == startrev:
 
60
                skip = False
 
61
            if skip:
 
62
                continue
 
63
            yield log
 
64
            if log.continuation_of:
 
65
                nextrev = log.continuation_of # sure it should not be startrev?
 
66
# pretty sure.  if we did that, startrev would never be None
 
67
                break
 
68
        startrev = nextrev
 
69
    if log and log.continuation_of:
 
70
        yield log.continuation_of
 
71
 
 
72
 
 
73
def iter_ancestry(tree, startrev=None):
 
74
    """Iterate over ancestry according to tree's patchlogs.
 
75
 
 
76
    Generator that yields `arch.Revision` instances according to `tree`
 
77
    ancestry.  Each `arch.Revision` returned is the direct ancestor of the
 
78
    previous `arch.Revision`.  If `startrev` is supplied, it is the first item
 
79
    yielded. The iteration stops after the first ancestor named in a patchlog
 
80
    that does not, itself, have a patchlog in `tree`, or at the beginning.
 
81
 
 
82
    :param tree: tree to determine the ancestry of.
 
83
    :type tree: `arch.ArchSourceTree`
 
84
    :param startrev: optional revision to start at. Must be in the ancestry of
 
85
        `tree`. The latest revision is used if not supplied.
 
86
    :type startrev: `arch.Revision`
 
87
    :rtype: iterator of `arch.Revision`
 
88
    """
 
89
    if startrev is None:
 
90
        startrev = tree.tree_revision
 
91
    for r in _iter_ancestry(tree, startrev):
 
92
        if not isinstance(r, pybaz.Revision):
 
93
            r = r.revision
 
94
        yield r
 
95
 
 
96
 
 
97
def iter_ancestry_logs(tree, startrev=None):
 
98
    """Iterate over ancestry according to tree's patchlogs.
 
99
 
 
100
    Generator that yields `arch.Patchlog` instances according to `tree`
 
101
    ancestry.  Each `arch.Patchlog` returned is from the direct ancestor of the
 
102
    revision of the previous `arch.Patchlog`.  If `startrev` is supplied, it is
 
103
    the first item yielded. The iteration stops after the first ancestor named
 
104
    in a patchlog that does not, itself, have a patchlog in `tree`, or at the
 
105
    beginning.
 
106
 
 
107
    :param tree: tree to determine the ancestry of.
 
108
    :type tree: `arch.ArchSourceTree`
 
109
    :param startrev: optional revision to start at. Must be in the ancestry of
 
110
        `tree`. The latest revision is used if not supplied.
 
111
    :type startrev: `arch.Revision`
 
112
    :rtype: iterator of `arch.Patchlog`
 
113
    """
 
114
    if startrev is None:
 
115
        startrev = tree.tree_revision
 
116
    for r in _iter_ancestry(tree, startrev):
 
117
        if not isinstance(r, pybaz.Patchlog):
 
118
            r = pybaz.Patchlog(r)
 
119
        yield r
 
120
 
 
121
 
 
122
def get_entry_name(entry):
 
123
    """Returns a filename for the entry, preferring the MOD name
 
124
 
 
125
    :param entry: The entry to get a filename for
 
126
    :type entry: `ChangesetEntry`
 
127
    :return: The found file name
 
128
    :rtype: str
 
129
    """
 
130
    if entry.mod_name is not None:
 
131
        return entry.mod_name
 
132
    else:
 
133
        return entry.orig_name
 
134
 
 
135
def iter_changes(changeset_munger):
 
136
    entries = changeset_munger.get_entries()
 
137
    sorted_entries = list(entries.itervalues())
 
138
    sorted_entries.sort(util.cmp_func(get_entry_name))
 
139
 
 
140
    for entry in sorted_entries:
 
141
        if entry.orig_name and not entry.mod_name:
 
142
            if entry.orig_type == "dir":
 
143
                suf="/ "
 
144
            else:
 
145
                suf="  "
 
146
            yield pybaz.FileDeletion("D%s%s" % (suf, entry.orig_name[2:]), ' ')
 
147
 
 
148
    for entry in sorted_entries:
 
149
        if entry.mod_name and not entry.orig_name:
 
150
            if entry.mod_type == "dir":
 
151
                suf="/ "
 
152
            else:
 
153
                suf="  "
 
154
            yield pybaz.FileAddition("A%s%s" % (suf, entry.mod_name[2:]), ' ')
 
155
 
 
156
    for entry in sorted_entries:
 
157
        if entry.orig_name != entry.mod_name and entry.orig_name and \
 
158
            entry.mod_name:
 
159
            yield pybaz.FileRename("=> %s\t%s" % (entry.orig_name[2:], 
 
160
                                                 entry.mod_name[2:]), ' ')
 
161
        if entry.diff:
 
162
            yield pybaz.FileModification("M  %s" % entry.mod_name[2:], ' ')
 
163
 
 
164
        if entry.original or entry.modified:
 
165
            yield pybaz.FileModification("Mb %s" % entry.mod_name[2:], ' ')
 
166
 
 
167
        if entry.orig_perms or entry.mod_perms:
 
168
            if entry.mod_type == "file":
 
169
                yield pybaz.FilePermissionsChange("-- %s" % entry.mod_name[2:],
 
170
                                                 ' ')
 
171
            elif entry.mod_type == "dir":
 
172
                yield pybaz.FilePermissionsChange("-/ %s" % entry.mod_name[2:],
 
173
                                                 ' ')
 
174
 
 
175
def diff_iter2(changeset, 
 
176
               exclude_str='(^\./{arch\}|/(\.arch-ids|\.arch-inventory))'):
 
177
    """Native diff iterator
 
178
 
 
179
    :param changeset: The changeset to iterate through the diffs of
 
180
    :type changeset: str
 
181
    """
 
182
    munger = native.ChangesetMunger(changeset)
 
183
    munger.read_indices()
 
184
    sorted_entries = list(munger.get_entries().itervalues())
 
185
    sorted_entries.sort(util.cmp_func(get_entry_name))
 
186
    if exclude_str is not None:
 
187
        exclude = re.compile(exclude_str)
 
188
    else:
 
189
        exclude = None
 
190
    for entry in sorted_entries:
 
191
        if exclude is not None and munger.entry_matches_pattern(entry, exclude):
 
192
            continue
 
193
        if entry.diff is not None:
 
194
            lines = [f.rstrip('\n') for f in open(entry.diff)]
 
195
        elif entry.mod_name is None or entry.orig_name is None:
 
196
            if entry.removed_file is None and entry.new_file is None:
 
197
                continue
 
198
            try:
 
199
                diff = native.invoke_diff(changeset+"/removed-files-archive",
 
200
                                          entry.orig_name,
 
201
                                          changeset+"/new-files-archive",
 
202
                                          entry.mod_name)
 
203
                lines = diff.split('\n')
 
204
            except native.BinaryFiles:
 
205
                continue
 
206
        else:
 
207
            continue
 
208
        for line in native.diff_classifier(lines):
 
209
            yield line
 
210
 
 
211
 
 
212
def replay(tree, revision, linehandler=None, reverse=False, munge_opts=None):
 
213
    """Applies the changeset for a revision to the tree
 
214
 
 
215
    :param tree: The tree to apply changes for
 
216
    :type tree: `arch.WorkingTree`
 
217
    :param revision: The revision to get the changes for
 
218
    :type revision: `arch.Revision`
 
219
    :param reverse: Apply the changeset in reverse
 
220
    :type reverse: bool
 
221
    :param munge_opts: options for munging the changeset before applying
 
222
    :type munge_opts: `MungeOpts`
 
223
    """
 
224
    #revision = NativeRevision(str(revision))
 
225
    my_dir = util.tmpdir(os.getcwd())
 
226
    try:
 
227
        changeset = revision.get_patch(my_dir)
 
228
        munger = native.ChangesetMunger(changeset)
 
229
        munger.read_indices()
 
230
        if munge_opts is not None:
 
231
            munger.munge(munge_opts)
 
232
#        entry_handler = PatchEntryHandler(tree)
 
233
        entry_handler = None
 
234
        iterator = iter_custom_apply_changeset(tree, munger, entry_handler, reverse=reverse)
 
235
        for line in iterator:
 
236
            if linehandler is not None:
 
237
                linehandler(line)
 
238
    finally:
 
239
        if os.access(my_dir, os.R_OK):
 
240
            shutil.rmtree(my_dir)
 
241
    try:
 
242
        if iterator.conflicting:
 
243
            return 1
 
244
        else:
 
245
            return 0
 
246
    except:
 
247
        return 2
 
248
 
 
249
 
 
250
def revert(tree, revision, munge_opts=None, keep=True):
 
251
    """Apply the difference between two things to tree.
 
252
 
 
253
    :param tree: The tree to apply changes to
 
254
    :type tree: `arch.ArchSourceTree`
 
255
    :param revision: The revision to apply the changes to
 
256
    :type revision: `arch.Revision`
 
257
    :param munge_opts: If supplied, a set of parameters for munging the changeset
 
258
    :type munge_opts: `MungeOpts`
 
259
    :param keep: If true, keep the generated changeset
 
260
    :type keep: bool
 
261
    :rtype: Iterator of changeset application output 
 
262
    """
 
263
    changeset = pybaz.util.new_numbered_name(tree, ',,undo-')
 
264
    delt=pybaz.iter_delta(revision, tree, changeset)
 
265
    for line in delt:
 
266
        if not isinstance(line, pybaz.TreeChange) and not chattermatch(line, "changeset:"):
 
267
            yield line
 
268
    if munge_opts is not None:
 
269
        yield pybaz.Chatter("* munging changeset")
 
270
        munger = native.ChangesetMunger(delt.changeset)
 
271
        entries = munger.munge(munge_opts)
 
272
        if entries == 0 and munge_opts.keep_pattern is not None:
 
273
            print "No entries matched pattern %s" % munge_opts.keep_pattern
 
274
    yield pybaz.Chatter("* applying changeset")
 
275
    for line in delt.changeset.iter_apply(tree, reverse=True):
 
276
        yield line
 
277
    if not keep:
 
278
        shutil.rmtree(changeset)
 
279
 
 
280
 
 
281
def iter_added_log(logs, revision):
 
282
    """Generate an iterator of revisions that added a certain revision log.
 
283
 
 
284
    :param logs: An iterator of patchlogs to filter
 
285
    :type logs: iter of `arch.Patchlog`
 
286
    :param revision: The revision of the log to look for
 
287
    :type revision: `arch.Revision`
 
288
    :rtype: iter of `arch.Revision`
 
289
    """
 
290
    for log in logs:
 
291
        if revision in log.new_patches:
 
292
            yield log
 
293
 
 
294
 
 
295
def merge_ancestor2(mine, other, other_revision):
 
296
    """Determines an ancestor suitable for merging, according to the tree logs.
 
297
 
 
298
    :param mine: Tree to find an ancestor for (usually the working tree)
 
299
    :type mine: `arch.WorkingTree`
 
300
    :param other: Tree that merged or was merged by mine
 
301
    :type other: `arch.ArchSourceTree`
 
302
    :param other_revision: Override for broken library revisions
 
303
    :type other_revision: `arch.Revision`
 
304
    :return: Log of the merged revision
 
305
    :rtype: `arch.Patchlog`
 
306
    """
 
307
    # find latest ancestor of opposite tree in each tree
 
308
    my_revision=tree_latest(mine)
 
309
    my_merged=last_merge_of(other, iter_ancestry(mine, my_revision))
 
310
    other_merged=last_merge_of(mine, iter_ancestry(other, other_revision))
 
311
    if my_merged is None:
 
312
        if other_merged is None:
 
313
            raise errors.UnrelatedTrees(mine, other)
 
314
        return other_merged
 
315
    if other_merged is None:
 
316
        return my_merged
 
317
 
 
318
    # determine which merge happend later in sequence for each tree
 
319
    other_latest = latest_merge(other, other_revision, other_merged, my_merged)
 
320
    my_latest = latest_merge(mine, my_revision, my_merged, other_merged)
 
321
    if other_latest != my_latest:
 
322
        #probably paralel merge
 
323
        raise errors.NoLatestRevision(my_latest, other_latest)
 
324
 
 
325
    return my_latest
 
326
 
 
327
 
 
328
def last_merge_of(into, ancestry):
 
329
    """Determine last log from the ancestry of "merged" in "into".
 
330
 
 
331
    :param into: The tree that a log was merged into
 
332
    :type into: `arch.ArchSourceTree`
 
333
    :param ancestry: An interator of the tree's ancestry
 
334
    :type ancestry: iter of `arch.Patchlog`
 
335
    :return: The last merged log, or None if none found
 
336
    :rtype: `arch.Revision`
 
337
    """
 
338
    log_list = None
 
339
    log_version = None
 
340
 
 
341
    for ancestor in ancestry:
 
342
        if ancestor.version != log_version:
 
343
            log_version = ancestor.version
 
344
            log_list = list([f.revision for f in into.iter_logs(log_version)])
 
345
        if ancestor in log_list:
 
346
            return ancestor
 
347
    return None
 
348
 
 
349
 
 
350
def latest_merge(tree, tree_revision, tree_merged, other_merged):
 
351
    """Determines which of two revisions was merged most recently into tree
 
352
    :param tree: The tree the revisions were merged into
 
353
    :type tree: `arch.ArchSourceTree`
 
354
    :param tree_merged: The revision from tree that was merged
 
355
    :type tree_merged: `arch.Revision`
 
356
    :param other_merged: The revision from OTHER that was merged
 
357
    :type other_merged: `arch.Revision`
 
358
    :return: The revision merged most recently, according to this tree
 
359
    :rtype: `arch.Revision`
 
360
    """
 
361
 
 
362
    for log in iter_ancestry_logs(tree, tree_revision):
 
363
        if log.revision == tree_merged:
 
364
            return tree_merged
 
365
        # skip logs that are continuations, because of bogus new-patches
 
366
        if log.continuation_of:
 
367
            continue
 
368
        if other_merged in log.new_patches:
 
369
            return other_merged
 
370
    return None
 
371
 
 
372
 
 
373
def iter_vchange(iterator):
 
374
    """First revision of each sequence from a different package-version.
 
375
 
 
376
    Generator that yields only those revisions in different
 
377
    package-versions than the previous revision.
 
378
 
 
379
    :type iterator: iterator of `arch.Revision`
 
380
    :rtype: iterator of `arch.Revision`
 
381
    """
 
382
    oldversion = None
 
383
    for item in iterator:
 
384
        if oldversion is not None and item.version != oldversion:
 
385
            yield item
 
386
        oldversion = item.version
 
387
 
 
388
 
 
389
def tag_source(tree):
 
390
    """Return the revision that the current tree thinks it was tagged from.
 
391
 
 
392
    :type tree: `arch.ArchSourceTree`
 
393
    :rtype: `arch.Revision`
 
394
    """
 
395
    try:
 
396
        return iter_vchange(iter_ancestry(tree, None)).next()
 
397
    except StopIteration:
 
398
        return None
 
399
 
 
400
 
 
401
def ensure_revision_exists(revision):
 
402
    pass
 
403
 
 
404
 
 
405
def ensure_archive_registered(archive, location=None):
 
406
    """Throw an exception if the archive isn't registered.
 
407
    This function is intended to be overridden.
 
408
 
 
409
    :param archive: The archive to check
 
410
    :type archive: `arch.Archive`
 
411
    :param location: A possible location of the archive
 
412
    :type location: str
 
413
    """
 
414
    if archive.is_registered():
 
415
        return
 
416
    else:
 
417
        raise pybaz.errors.ArchiveNotRegistered(archive)
 
418
 
 
419
 
 
420
def tree_file_path(tree, path):
 
421
    """Convert a relative or absolute path into a tree-relative path
 
422
 
 
423
    :param tree: The tree the path falls in
 
424
    :param path: The path the tree falls in
 
425
    """
 
426
    dir, file = os.path.split(path)
 
427
    if dir == "":
 
428
        dir = "."
 
429
    dir = os.path.realpath(dir)
 
430
    treedir = os.path.realpath(tree)
 
431
    if not dir.startswith(treedir):
 
432
        raise errors.TreeNotContain(tree, path)
 
433
    dir = dir[len(treedir)+1:]
 
434
    return os.path.join(dir, file)
 
435
 
 
436
def tree_cwd(tree):
 
437
    """Get the current directory expressed as a tree path.
 
438
    :param tree: The tree that the current directory is in
 
439
    :type tree: ArchSourceTree
 
440
    :return: The tree path
 
441
    :raise TreeNotContain: The path does not fall in the tree
 
442
    """
 
443
    cwd = os.getcwd()
 
444
    if not cwd.startswith(str(tree)):
 
445
        raise errors.TreeNotContain(tree, cwd)
 
446
    treepath = cwd[len(str(tree)):]
 
447
    treepath = treepath.lstrip('/')
 
448
    return treepath
 
449
 
 
450
 
 
451
def iter_changedfile(tree, revision, filename, yield_file=False):
 
452
    """Lists logs of ancestor revisions that changed a particular file.  
 
453
    Revisions that do not have patchlogs in the tree are ignored.
 
454
 
 
455
    :param tree: The tree to get logs from
 
456
    :type tree: `arch.ArchSourceTree`
 
457
    :param revision: The revision to trace ancestry from
 
458
    :type revision: `arch.Revision`
 
459
    :param filename: The filename to look for changes to
 
460
    :type filename: str
 
461
    :rtype: iterator of `arch.Patchlog`
 
462
    """
 
463
    cwd = tree_cwd(tree)
 
464
    if cwd != "":
 
465
        filename = cwd + "/" + filename
 
466
    for log in _iter_ancestry(tree, revision):
 
467
        if isinstance(log, pybaz.Patchlog):
 
468
            if filename in log.modified_files or filename in log.new_files or filename in log.removed_files:
 
469
                if yield_file:
 
470
                    yield (filename, log)
 
471
                else:
 
472
                    yield log
 
473
            if log.renamed_files:
 
474
                for (key, value) in log.renamed_files.iteritems():
 
475
                    if value == filename:
 
476
                        filename = key
 
477
                        break
 
478
 
 
479
def iter_changed_file_line(tree, revision, filename, line_num):
 
480
    """Lists logs of ancestor revisions that changed a particular file.  
 
481
    Revisions that do not have patchlogs in the tree are ignored.
 
482
 
 
483
    :param tree: The tree to get logs from
 
484
    :type tree: `arch.ArchSourceTree`
 
485
    :param revision: The revision to trace ancestry from
 
486
    :type revision: `arch.Revision`
 
487
    :param filename: The filename to look for changes to
 
488
    :type filename: str
 
489
    :rtype: iterator of `arch.Patchlog`
 
490
    """
 
491
    cwd = tree_cwd(tree)
 
492
    if cwd != "":
 
493
        cwd += ("/")
 
494
 
 
495
    patches = []
 
496
    for (filename, log) in iter_changedfile(tree, revision, filename, True):
 
497
        path = filename
 
498
        if log.revision.patchlevel == "base-0" and log.continuation_of is None:
 
499
            continue
 
500
        if filename in log.new_files:
 
501
            continue
 
502
        if filename in log.removed_files:
 
503
            continue
 
504
        patch = parse_changeset_patches(log.revision,[path])[0]
 
505
        if patch is None:
 
506
            continue
 
507
        new_iter = patch.iter_inserted()
 
508
        for (num, line) in new_iter:
 
509
            old_num = num
 
510
 
 
511
            for cur_patch in patches:
 
512
                num = cur_patch.pos_in_mod(num)
 
513
                if num == None: 
 
514
                    break
 
515
            if num == line_num - 1:
 
516
                yield log
 
517
        patches=[patch]+patches
 
518
 
 
519
 
 
520
class AnnotateLine:
 
521
    """A line associated with the log that produced it"""
 
522
    def __init__(self, text, log=None):
 
523
        self.text = text
 
524
        self.log = log
 
525
 
 
526
 
 
527
def annotate_file(tree, revision, filename):
 
528
    """Lists logs of ancestor revisions that changed a particular file.  
 
529
    Revisions that do not have patchlogs in the tree are ignored.
 
530
 
 
531
    :param tree: The tree to get logs from
 
532
    :type tree: `arch.ArchSourceTree`
 
533
    :param revision: The revision to trace ancestry from
 
534
    :type revision: `arch.Revision`
 
535
    :param filename: The filename to look for changes to
 
536
    :type filename: str
 
537
    :rtype: iterator of `arch.Patchlog`
 
538
    """
 
539
    lines = [AnnotateLine(f) for f in open(filename)]
 
540
 
 
541
    patches = []
 
542
    for (filename, log) in iter_changedfile(tree, revision, filename, True):
 
543
        patch = None
 
544
        path = filename
 
545
        if log.revision.patchlevel == "base-0" and log.continuation_of is None:
 
546
            continue
 
547
        if filename in log.new_files:
 
548
            new_iter = iter_new_file(log.revision, path)
 
549
        else:
 
550
            patch = parse_changeset_patches(log.revision,[path])[0]
 
551
            if patch is None: 
 
552
                continue
 
553
            new_iter = patch.iter_inserted()
 
554
        for (num, line) in new_iter:
 
555
            old_num = num
 
556
 
 
557
            for cur_patch in patches:
 
558
                num = cur_patch.pos_in_mod(num)
 
559
                if num == None: 
 
560
                    break
 
561
 
 
562
            if num is not None and lines[num].log is None:
 
563
                lines[num].log = log
 
564
        if patch is None:
 
565
            break
 
566
        patches=[patch]+patches
 
567
    return lines
 
568
 
 
569
 
 
570
def print_annotated(lines):
 
571
    """Unprettily-prints a set of annotated lines from annotate_file"""
 
572
    last = "NotNoneOrPatchlog"
 
573
    for line in lines:
 
574
        if line.log != last:
 
575
            if line.log == None:
 
576
                print "UNASSIGNED"
 
577
            else:
 
578
                print line.log.revision
 
579
            last=line.log
 
580
        print("  " + line.text.rstrip("\n"))
 
581
 
 
582
def iter_changed_file_line_orig(tree, revision, filename, line_num):
 
583
    """Lists logs of revisions that originally added a line to a particular
 
584
    file.  Revisions that do not have patchlogs in the tree are ignored.
 
585
 
 
586
    :param tree: The tree to get logs from
 
587
    :type tree: `arch.ArchSourceTree`
 
588
    :param revision: The revision to trace ancestry from
 
589
    :type revision: `arch.Revision`
 
590
    :param filename: The filename to look for changes to
 
591
    :type filename: str
 
592
    :rtype: iterator of `arch.Patchlog`
 
593
    """
 
594
    path = tree_cwd(tree)+"/"+filename
 
595
    my_file = open(filename)
 
596
    try:
 
597
        for i in range(line_num):
 
598
            line = my_file.next()
 
599
    except StopIteration:
 
600
        raise ValueError("line number out of range")
 
601
    for log in iter_changed_file_line(tree, revision, filename, line_num):
 
602
        yield merge_blame(log, filename, path, line)
 
603
 
 
604
 
 
605
def merge_blame(log, filename, path, line, diffs=None):
 
606
    """Return the log of the revision that added a line.
 
607
 
 
608
    :param log: The log of the revision that added a line
 
609
    :type log: `arch.Patchlog`
 
610
    :param filename: The name of the file to look for
 
611
    :type filename: str
 
612
    :param path: The path of the file to look for
 
613
    :type path: str
 
614
    :param line: The added line
 
615
    :type line: str
 
616
    """
 
617
    for revision in log.new_patches:
 
618
        if revision == log.revision:
 
619
            continue
 
620
        if filename in log.modified_files or filename in log.new_files:
 
621
            if patch_added_line(log.revision, filename, line, diffs):
 
622
                return merge_blame(revision.patchlog, filename, path, line,
 
623
                                   diffs)
 
624
    return log
 
625
 
 
626
def get_diff(revision, path, diffs):
 
627
    """Returns the diff for a given path, memoizing it in diffs.
 
628
    :param revision: The revision to find the diff for
 
629
    :type revision: `arch.Revision`
 
630
    :param path: The path of the diff to get
 
631
    :type path: str
 
632
    :param diffs: The diff will be placed in diffs if it's not already there
 
633
    :type diffs: map of str => str
 
634
    :return: lines of the diff
 
635
    """
 
636
    if diffs is not None:
 
637
        diff = diffs.get(str(revision))
 
638
    else:
 
639
        diff = None
 
640
 
 
641
    if diff is not None:
 
642
        return diff
 
643
    
 
644
    tree = util.tmpdir()
 
645
    try:
 
646
        ensure_archive_registered(revision.archive)
 
647
        changeset = revision.get_patch(tree+"/changeset")
 
648
        patch_path = "%s/patches/%s.patch" % (changeset, path)
 
649
        if not os.access(patch_path, os.R_OK):
 
650
            return None
 
651
        diff = list(open(patch_path))
 
652
        if diffs is not None:
 
653
            diffs[str(revision)] = diff
 
654
    finally:
 
655
        shutil.rmtree(tree)
 
656
    return diff
 
657
 
 
658
def patch_added_line(revision, path, line, diffs=None):
 
659
    """Determine whether a revision's changeset added a line.
 
660
 
 
661
    :param revision: The log of the revision that may have added a line
 
662
    :type revision: `arch.Revision`
 
663
    :param path: The path of the file to look for
 
664
    :type path: str
 
665
    :param line: The added line
 
666
    :type line: str
 
667
    """
 
668
    diff = get_diff(revision, path, diffs)
 
669
    if diff is None:
 
670
        return False
 
671
    for pline in diff:
 
672
        match = (pline.startswith('+') and pline[1:] == line)
 
673
        if match:
 
674
            break
 
675
    return match
 
676
 
 
677
 
 
678
def parse_changeset_patches(revision, paths):
 
679
    """Retrieves the requested patches from a revision changeset.
 
680
 
 
681
    :param revision: The revision of the changeset
 
682
    :type revision: `arch.Revision`
 
683
    :param paths: The path of the file to get patches for
 
684
    :type path: str
 
685
    :return: list of patches, in order of request
 
686
    :rtype: list of `patches.Patch`
 
687
    """
 
688
    tree = util.tmpdir()
 
689
    ensure_archive_registered(revision.archive)
 
690
    changeset = revision.get_patch(tree+"/changeset")
 
691
    patchen = []
 
692
    for path in paths:
 
693
        patch_path = "%s/patches/%s.patch" % (changeset, path)
 
694
        if os.path.exists(patch_path):
 
695
            patchen.append(patches.parse_patch(open(patch_path)))
 
696
        else:
 
697
            patchen.append(None)
 
698
    shutil.rmtree(tree)
 
699
    return patchen 
 
700
 
 
701
 
 
702
def iter_new_file(revision, path):
 
703
    """Retrieves the requested patches from a revision changeset.
 
704
 
 
705
    :param revision: The revision of the changeset
 
706
    :type revision: `arch.Revision`
 
707
    :param paths: The path of the file to get patches for
 
708
    :type path: str
 
709
    :return: list of patches, in order of request
 
710
    :rtype: list of `patches.Patch`
 
711
    """
 
712
    tree = util.tmpdir()
 
713
    ensure_archive_registered(revision.archive)
 
714
    changeset = revision.get_patch(tree+"/changeset")
 
715
    file_path = "%s/new-files-archive/%s" % (changeset, path)
 
716
    i = 0
 
717
    lines = []
 
718
    for line in open(file_path):
 
719
        lines.append((i, line))
 
720
        i += 1
 
721
    shutil.rmtree(tree)
 
722
    for line in lines:
 
723
        yield line
 
724
 
 
725
 
 
726
def iter_log_match(iter, key, value):
 
727
    for log in iter:
 
728
        if isinstance(log, pybaz.Revision):
 
729
            log = pybaz.Patchlog(log)
 
730
        if log[key] == value:
 
731
            yield log
 
732
 
 
733
 
 
734
def tree_latest(tree, version=None):
 
735
    """Return the latest Revision in the tree
 
736
 
 
737
    :param tree: The tree to return the revision for
 
738
    :type tree: `arch.ArchSourceTree`
 
739
    :param version: The version of the revision to find
 
740
    :type version: `arch.Version`
 
741
    """
 
742
    if version is None:
 
743
        version = tree.tree_version
 
744
    try:
 
745
        log = tree.iter_logs(version = version, reverse = True).next()
 
746
        return log.revision
 
747
    except StopIteration:
 
748
        raise errors.NoVersionRevisions(tree, version)
 
749
 
 
750
def iter_all_library_revisions():
 
751
    """Generates an iterator of all revisions in the library
 
752
    :return: iterator of all revisions in the revision library
 
753
    :rtype: iter of `arch.Revision`
 
754
    """
 
755
    for archive in pybaz.iter_library_archives():
 
756
        for category in archive.iter_library_categories():
 
757
            for branch in category.iter_library_branches():
 
758
                for version in branch.iter_library_versions():
 
759
                    for revision in version.iter_library_revisions():
 
760
                        yield revision
 
761
 
 
762
 
 
763
def is_library_dir(directory):
 
764
    directory = os.path.realpath(directory)
 
765
    for revlib in arch_core.iter_revision_libraries():
 
766
        if dir == revlib:
 
767
            return True
 
768
    return False
 
769
 
 
770
 
 
771
def iter_greedy_libraries():
 
772
    """Generate an iterator of greedy libraries.
 
773
    :rtype: iter of str
 
774
    """
 
775
    for path in arch_core.iter_revision_libraries():
 
776
        if os.access("%s/=greedy" % path, os.R_OK):
 
777
            yield path
 
778
 
 
779
 
 
780
def find_or_make_local_revision(revision):
 
781
    """Finds or makes a library copy of a revision.
 
782
    :param revision: The revision to look for
 
783
    :type revision: `arch.Revision`
 
784
    :return: The library copy
 
785
    :rtype: `arch.LibraryTree`
 
786
    """
 
787
    try:
 
788
        return revision.library_find()
 
789
    except:
 
790
        ensure_revision_exists(revision)
 
791
        for lib in iter_greedy_libraries():
 
792
            list(arch_core.iter_library_add(revision, lib))
 
793
            return revision.library_find()
 
794
    raise errors.NoGreedy(revision)
 
795
 
 
796
def list_mod_names(tree):
 
797
    tmpdir = util.tmpdir()+"/changeset"
 
798
    modified = []
 
799
    try:
 
800
        for line in pybaz.iter_delta(tree_latest(tree), tree, tmpdir):
 
801
            if isinstance(line, pybaz.TreeChange):
 
802
                modified.append(line.name[1:])
 
803
    finally:
 
804
        shutil.rmtree(tmpdir)
 
805
    return modified
 
806
 
 
807
def list_mod(tree, revision=None):
 
808
    tmpdir = util.tmpdir()+"/changeset"
 
809
    modified = []
 
810
    if revision is None:
 
811
        revision = tree_latest(tree)
 
812
    try:
 
813
        for line in pybaz.iter_delta(revision, tree, tmpdir):
 
814
            if isinstance(line, pybaz.TreeChange):
 
815
                if line.name[0]==" ":
 
816
                    line.name = line.name[1:]
 
817
                modified.append(line)
 
818
    finally:
 
819
        shutil.rmtree(tmpdir)
 
820
    return modified
 
821
 
 
822
def valid_tree_root(tree):
 
823
    if tree is None:
 
824
        raise errors.TreeRootNone()
 
825
 
 
826
 
 
827
def iter_no_conflicts(iter_rev, test_dir, good_dir, reverse=False):
 
828
    """Generate an iterator of revisions/changesets that don't conflict.
 
829
    revision,changeset tuples are returned, but the changeset is None if
 
830
    applying it would produce conflicts.
 
831
 
 
832
    :param iter_rev: the iterator of revisions to go through
 
833
    :type iter_rev: iterator of `arch.Revision`
 
834
    :param test_dir: The scratch directory to use
 
835
    :type test_dir: str
 
836
    :param good_dir: The good directory to copy from
 
837
    :type good_dir: str
 
838
    :param reverse: Apply revisions in reverse?
 
839
    :type reverse: bool
 
840
    :rtype: iter of (revision, changeset) 
 
841
    """
 
842
    for revision in iter_rev:
 
843
        if revision == None:
 
844
            continue
 
845
        if isinstance(revision, pybaz.Patchlog):
 
846
            revision = revision.revision
 
847
        directory = util.tmpdir()
 
848
        tree = pybaz.tree_root(test_dir)
 
849
        changeset = revision.get_patch(directory+"/changeset")
 
850
        result = arch_core.apply_changeset(tree, changeset, 
 
851
                                           reverse=reverse)
 
852
        if result:
 
853
            changeset = None
 
854
            shutil.rmtree(test_dir)
 
855
            util.linktree(good_dir, test_dir)
 
856
 
 
857
        yield (revision, changeset)
 
858
        shutil.rmtree(directory)
 
859
 
 
860
def iter_no_conflicts_rev(iter_rev, test_dir, good_dir, reverse=False):
 
861
    """Generate an iterator of revisions/changesets that don't conflict.
 
862
    
 
863
    :param iter_rev: the iterator of revisions to go through
 
864
    :type iter_rev: iterator of `arch.Revision`
 
865
    :param test_dir: The scratch directory to use
 
866
    :type test_dir: str
 
867
    :param good_dir: The good directory to copy from
 
868
    :type good_dir: str
 
869
    :param reverse: Apply revisions in reverse?
 
870
    :type reverse: bool
 
871
    :rtype: iter of revision 
 
872
    """
 
873
    for (revision, changeset) in \
 
874
        iter_no_conflicts(iter_rev, test_dir, good_dir, reverse):
 
875
        if changeset is not None:
 
876
            shutil.rmtree(changeset)
 
877
            yield revision
 
878
 
 
879
 
 
880
def iter_skip_replay_conflicts(tree, version):
 
881
    """Generate an iterator of revisions/changesets that won't conflict if
 
882
    replayed.  Assumes that all revisions returned are applied.
 
883
    
 
884
    :param tree: the tree that revisions might conflict with
 
885
    :type tree: `arch.WorkingTree`
 
886
    :param version: 
 
887
    :param reverse: Apply revisions in reverse?
 
888
    """
 
889
    iter_miss = arch_core.iter_missing(tree, version, False)
 
890
    test_dir = util.tmpdir(tree)
 
891
    try:
 
892
        util.linktree(tree, test_dir, top_exists = True)
 
893
        iter_no_conf = iter_no_conflicts_rev(iter_miss, test_dir, tree)
 
894
        return util.iter_delete_wrapper(iter_no_conf, test_dir)
 
895
    except:
 
896
        shutil.rmtree(test_dir)
 
897
        raise
 
898
 
 
899
 
 
900
class iter_depends:
 
901
    def __init__(self, anc_iter, nondeps=True):
 
902
        self.anc_iter = anc_iter
 
903
        self.nondeps=nondeps
 
904
        try:
 
905
            revision = anc_iter.next()
 
906
            if isinstance(revision, pybaz.Patchlog):
 
907
                revision = revision.revision
 
908
            self.treedir = util.tmpdir(os.getcwd())
 
909
            self.good = self.treedir+"/good"
 
910
            self.test = self.treedir+"/test"
 
911
            list(arch_core.iter_get(revision, self.good))
 
912
            util.linktree(self.good, self.test)
 
913
        except StopIteration:
 
914
            self.treedir = None
 
915
 
 
916
    def __del__(self):
 
917
        if self.treedir is not None:
 
918
            shutil.rmtree(self.treedir)
 
919
 
 
920
    def __iter__(self):
 
921
        if self.treedir is None:
 
922
            return
 
923
        revision = None
 
924
        for newrevision in self.anc_iter:
 
925
            if revision == None:
 
926
                revision = newrevision
 
927
                continue
 
928
            if isinstance(revision, pybaz.Patchlog):
 
929
                revision = revision.revision
 
930
            directory = util.tmpdir()
 
931
            try:
 
932
                changeset = revision.get_patch(directory+"/changeset")
 
933
                result = arch_core.apply_changeset(pybaz.tree_root(self.test),
 
934
                                                   changeset, reverse=True)
 
935
                if result:
 
936
                    arch_core.apply_changeset(pybaz.tree_root(self.good),
 
937
                                              changeset, reverse=True)
 
938
            except:
 
939
                shutil.rmtree(directory)
 
940
                raise
 
941
            
 
942
            shutil.rmtree(directory)
 
943
            if (result == 0) == self.nondeps:
 
944
                yield revision
 
945
            if result != 0:
 
946
                shutil.rmtree(self.test)
 
947
                shutil.copytree(self.good, self.test)
 
948
            revision = newrevision
 
949
 
 
950
 
 
951
class LogMemo:
 
952
    """Memoized access to tree logs"""
 
953
    def __init__(self, tree):
 
954
        self.tree = tree
 
955
        self.logs = {}
 
956
 
 
957
    def get_log(self, revision):
 
958
        """From tree, get the log for this revision.
 
959
        
 
960
        :param revision: The revision to get
 
961
        :type revision: `arch.Revision`
 
962
        """
 
963
        if not self.logs.has_key(revision.version):
 
964
            self.logs[str(revision.version)] = \
 
965
                list(self.tree.iter_logs(revision.version))
 
966
        for log in self.logs[str(revision.version)]:
 
967
            if log.revision == revision:
 
968
                return log
 
969
        return None
 
970
 
 
971
 
 
972
def iter_to_present(iter, tree):
 
973
    """Returns all logs until it encounters new-patch present in the tree
 
974
 
 
975
    :param iter: The iterator that produces logs/revisions
 
976
    :type iter: iter of `arch.Patchlog` or `arch.Revision`
 
977
    """
 
978
    log_memo = LogMemo(tree)
 
979
    for log in iter:
 
980
        if isinstance(log, pybaz.Revision):
 
981
            log = log.patchlog
 
982
        for revision in log.new_patches:
 
983
            if log_memo.get_log(revision) is not None:
 
984
                return
 
985
        yield log
 
986
 
 
987
 
 
988
def changelog_for_merge(logs, merges):
 
989
    """Produces a new-format log-for-merge that includes merged log bodies
 
990
 
 
991
    :param logs: The list or iter of logs to use.  Direct merges recommended.
 
992
    :type logs: iter of `arch.Patchlog`
 
993
    :return: A string containing log-for-merge text
 
994
    :rtype: str
 
995
    """
 
996
    changelog = ""
 
997
    for log in logs:
 
998
        if changelog != "":
 
999
            changelog += "\n"
 
1000
        changelog += " * %s\n" % log.revision
 
1001
        changelog += " * %s\n" % log.summary
 
1002
        if (log.description != ""):
 
1003
            has_bogus_merges = False
 
1004
            for merge in log.new_patches:
 
1005
                if merges is not None and not merge in [x.revision for x in 
 
1006
                                                        merges]:
 
1007
                    has_bogus_merges = True
 
1008
                    break
 
1009
            if not has_bogus_merges:
 
1010
                changelog += log.description
 
1011
    return changelog
 
1012
 
 
1013
 
 
1014
class EntryHandler:
 
1015
    def __init__(self, tree):
 
1016
        self.tree = tree
 
1017
        self.id_map = arch_core.get_id_filename_map(tree)
 
1018
 
 
1019
class PatchEntryHandler(EntryHandler):
 
1020
    def __init__(self, tree):
 
1021
        EntryHandler.__init__(self, tree)
 
1022
        self.removed_dir = None
 
1023
 
 
1024
    def __call__(self, entry, reverse):
 
1025
        if not entry.diff:
 
1026
            return None
 
1027
        status = self.invoke_patch(entry, reverse)
 
1028
        if status == 0:
 
1029
            result = pybaz.FileModification("M  %s" % entry.mod_name[2:], ' ')
 
1030
        elif status == 1:
 
1031
            result = pybaz.PatchConflict("C   %s" % entry.mod_name[2:])
 
1032
        else:
 
1033
            raise Exception("unexpected status %s" % str(status))
 
1034
        os.unlink(entry.diff)
 
1035
        return result
 
1036
 
 
1037
    def get_removed_dir(self):
 
1038
        if self.removed_dir is None:
 
1039
            self.removed_dir = tempfile.mkdtemp("", "+removed-conflict-files", 
 
1040
                                        self.tree)
 
1041
        return self.removed_dir
 
1042
 
 
1043
    def remove_conflict_files(self, file):
 
1044
        rej = file+".rej"
 
1045
        if not os.path.exists(rej):
 
1046
            rej = None
 
1047
        orig = file+".orig"
 
1048
        if not os.path.exists(orig):
 
1049
            orig = None
 
1050
        if orig is not None or rej is not None:
 
1051
            removed_dir = self.get_removed_dir()
 
1052
            new_parent_dir = os.path.dirname(os.path.join(removed_dir,
 
1053
                                             file[len(self.tree):]))
 
1054
            if not os.path.exists(new_parent_dir):
 
1055
                os.makedirs(new_parent_dir)
 
1056
 
 
1057
            if orig is not None:
 
1058
                os.rename(orig, os.path.join(new_parent_dir, 
 
1059
                          os.path.basename(orig)))
 
1060
            if rej is not None:
 
1061
                os.rename(rej, os.path.join(new_parent_dir, 
 
1062
                          os.path.basename(rej)))
 
1063
 
 
1064
    def invoke_patch(self, entry, reverse=False):
 
1065
        file = os.path.join(self.tree, self.id_map[entry.id])
 
1066
        self.remove_conflict_files(file)
 
1067
        new_version = util.NewFileVersion(file)
 
1068
        out_file = new_version.temp_filename
 
1069
        status = native.invoke_patch(file, entry.diff, out_file, reverse)
 
1070
        if status == 1:
 
1071
            os.link(file, file+".orig")
 
1072
            os.rename(out_file+".rej", file+".rej")
 
1073
        new_version.commit()
 
1074
        return status
 
1075
 
 
1076
 
 
1077
class Diff3Handler(PatchEntryHandler):
 
1078
    def __init__(self, tree, older_tree, yours_tree):
 
1079
        PatchEntryHandler.__init__(self, tree)
 
1080
        self.older_tree = older_tree
 
1081
        self.yours_tree = yours_tree
 
1082
        self.older_map = arch_core.get_id_filename_map(self.older_tree)
 
1083
        self.yours_map = arch_core.get_id_filename_map(self.yours_tree)
 
1084
 
 
1085
    def invoke_patch(self, entry, reverse=False):
 
1086
        mine_file = os.path.join(self.tree, self.id_map[entry.id])
 
1087
        older_path = os.path.join(self.older_tree, self.older_map[entry.id])
 
1088
        yours_path = os.path.join(self.yours_tree, self.yours_map[entry.id])
 
1089
        self.remove_conflict_files(mine_file)
 
1090
        new_version = util.NewFileVersion(mine_file)
 
1091
        out_file = new_version.temp_filename
 
1092
        if reverse:
 
1093
            status = native.invoke_diff3(new_version, mine_file, yours_path, 
 
1094
                                         older_path)
 
1095
        else:
 
1096
            status = native.invoke_diff3(new_version, mine_file, older_path,
 
1097
                                         yours_path)
 
1098
 
 
1099
        new_version.commit()
 
1100
        return status
 
1101
 
 
1102
 
 
1103
class iter_custom_apply_changeset:
 
1104
    def __init__(self, dir, munger, entry_handler, reverse=False):
 
1105
        self.dir = dir
 
1106
        self.munger = munger
 
1107
        self.entry_handler = entry_handler
 
1108
        self.reverse = reverse
 
1109
        self.conflicting = False
 
1110
        self.iterator = self.make_iterator()
 
1111
 
 
1112
    def __iter__(self):
 
1113
        """Destructively applies a changeset
 
1114
        """
 
1115
        return self.iterator
 
1116
 
 
1117
 
 
1118
    def next(self):
 
1119
        return self.iterator.next()
 
1120
 
 
1121
 
 
1122
    def make_iterator(self):
 
1123
        if self.entry_handler is not None:
 
1124
            for entry in self.munger.get_entries().itervalues():
 
1125
                result = self.entry_handler(entry, self.reverse)
 
1126
                if result is not None:
 
1127
                    if isinstance(result, pybaz.PatchConflict):
 
1128
                        self.conflicting = True
 
1129
                    yield result
 
1130
        changeset = pybaz.Changeset(self.munger.changeset)
 
1131
        ch_iter = changeset.iter_apply(self.dir, self.reverse)
 
1132
        for line in ch_iter:
 
1133
            yield line
 
1134
        if ch_iter.conflicting:
 
1135
            self.conflicting = True
 
1136
 
 
1137
def apply_delta_custom(a_spec, b_spec, tree, munge_opts=None, apply_type=None, 
 
1138
        reverse=False):
 
1139
    def spec_resolve(spec):
 
1140
        if isinstance(spec, pybaz.Revision):
 
1141
            return find_or_make_local_revision(spec)
 
1142
        else:
 
1143
            return spec
 
1144
 
 
1145
    from_path = spec_resolve(a_spec)
 
1146
    to_path = spec_resolve(b_spec)
 
1147
    
 
1148
    if apply_type == PatchEntryHandler:
 
1149
        entry_handler = PatchEntryHandler(tree, reverse=reverse)
 
1150
    elif apply_type == Diff3Handler:
 
1151
        entry_handler = Diff3Handler(tree, from_path, to_path)
 
1152
    else:
 
1153
        entry_handler = None
 
1154
 
 
1155
    tmp=util.tmpdir(tree)
 
1156
    changeset=tmp+"/changeset"
 
1157
    delt=pybaz.iter_delta(from_path, to_path, changeset)
 
1158
    for line in delt:
 
1159
        yield line
 
1160
    munger = native.ChangesetMunger(delt.changeset)
 
1161
    munger.read_indices()
 
1162
    if munge_opts is not None:
 
1163
        yield pybaz.Chatter("* munging changeset")
 
1164
        munger.munge(munge_opts)
 
1165
    yield pybaz.Chatter("* applying changeset")
 
1166
    for line in iter_custom_apply_changeset(tree, munger, entry_handler, reverse=reverse):
 
1167
        yield line
 
1168
    shutil.rmtree(tmp)
 
1169
 
 
1170
 
 
1171
def revision(name):
 
1172
    try:
 
1173
        return NativeRevision(name)
 
1174
    except errors.UnsupportedScheme, e:
 
1175
        return pybaz.Revision(name)
 
1176
 
 
1177
 
 
1178
class NativeRevision(pybaz.Revision):
 
1179
    def __init__ (self, name):
 
1180
        parsed = pybaz.NameParser(name)
 
1181
        self.arch_name = parsed.get_archive()
 
1182
        params = native.ArchParams()
 
1183
        if not params['=locations'].exists(self.arch_name):
 
1184
            raise pybaz.errors.ArchiveNotRegistered()
 
1185
        self.location = params['=locations'][self.arch_name].strip()
 
1186
        native.get_pfs_type(self.location)
 
1187
        pybaz.Revision.__init__(self, name)
 
1188
        
 
1189
    def narchive(self):
 
1190
        return native.get_pfs_archive(self.location, self.arch_name)
 
1191
 
 
1192
    def exists(self):
 
1193
        return self.narchive().exists(self)
 
1194
 
 
1195
    def get_patch(self, dir):
 
1196
        tempdir = tempfile.mkdtemp("", ",", 
 
1197
                                   os.path.dirname(os.path.realpath(dir)))
 
1198
        try:
 
1199
            patchfile = os.path.join(tempdir + "delta.tar.gz")
 
1200
            patch = native.cache_get_revision(self, "delta.tar.gz")
 
1201
            if patch is None:
 
1202
                patch = self.narchive().get_patch(self)
 
1203
            file(patchfile, "wb").write(patch.read())
 
1204
            parent_dir = util.untar_parent(patchfile, tempdir)
 
1205
            os.rename(os.path.join(tempdir, parent_dir), dir)
 
1206
 
 
1207
        finally:
 
1208
            shutil.rmtree(tempdir)
 
1209
        return pybaz.Changeset(dir)
 
1210
 
 
1211
# arch-tag: c73c601a-0050-4e6c-ac63-819d7a00921f