/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Aaron Bentley
  • Date: 2006-02-16 18:52:17 UTC
  • mto: (1558.1.4 Aaron's integration)
  • mto: This revision was merged to the branch mainline in revision 1565.
  • Revision ID: abentley@panoramicfeedback.com-20060216185217-c766d147d91191d8
Added progress bars to merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import os
 
18
import errno
 
19
from stat import S_ISREG
 
20
 
 
21
from bzrlib import BZRDIR
 
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
 
23
                           ReusingTransform, NotVersionedError, CantMoveRoot,
 
24
                           ExistingLimbo, ImmortalLimbo)
 
25
from bzrlib.inventory import InventoryEntry
 
26
from bzrlib.osutils import file_kind, supports_executable, pathjoin
 
27
from bzrlib.progress import DummyProgress
 
28
from bzrlib.trace import mutter
 
29
 
 
30
 
 
31
ROOT_PARENT = "root-parent"
 
32
 
 
33
 
 
34
def unique_add(map, key, value):
 
35
    if key in map:
 
36
        raise DuplicateKey(key=key)
 
37
    map[key] = value
 
38
 
 
39
 
 
40
class TreeTransform(object):
 
41
    """Represent a tree transformation."""
 
42
    def __init__(self, tree, pb=DummyProgress()):
 
43
        """Note: a write lock is taken on the tree.
 
44
        
 
45
        Use TreeTransform.finalize() to release the lock
 
46
        """
 
47
        object.__init__(self)
 
48
        self._tree = tree
 
49
        self._tree.lock_write()
 
50
        try:
 
51
            control_files = self._tree._control_files
 
52
            self._limbodir = control_files.controlfilename('limbo')
 
53
            try:
 
54
                os.mkdir(self._limbodir)
 
55
            except OSError, e:
 
56
                if e.errno == errno.EEXIST:
 
57
                    raise ExistingLimbo(self._limbodir)
 
58
        except: 
 
59
            self._tree.unlock()
 
60
            raise
 
61
 
 
62
        self._id_number = 0
 
63
        self._new_name = {}
 
64
        self._new_parent = {}
 
65
        self._new_contents = {}
 
66
        self._removed_contents = set()
 
67
        self._new_executability = {}
 
68
        self._new_id = {}
 
69
        self._non_present_ids = {}
 
70
        self._r_new_id = {}
 
71
        self._removed_id = set()
 
72
        self._tree_path_ids = {}
 
73
        self._tree_id_paths = {}
 
74
        self._new_root = self.get_id_tree(tree.get_root_id())
 
75
        self.__done = False
 
76
        self._pb = pb
 
77
 
 
78
    def __get_root(self):
 
79
        return self._new_root
 
80
 
 
81
    root = property(__get_root)
 
82
 
 
83
    def finalize(self):
 
84
        """Release the working tree lock, if held, clean up limbo dir."""
 
85
        if self._tree is None:
 
86
            return
 
87
        try:
 
88
            for trans_id, kind in self._new_contents.iteritems():
 
89
                path = self._limbo_name(trans_id)
 
90
                if kind == "directory":
 
91
                    os.rmdir(path)
 
92
                else:
 
93
                    os.unlink(path)
 
94
            try:
 
95
                os.rmdir(self._limbodir)
 
96
            except OSError:
 
97
                # We don't especially care *why* the dir is immortal.
 
98
                raise ImmortalLimbo(self._limbodir)
 
99
        finally:
 
100
            self._tree.unlock()
 
101
            self._tree = None
 
102
 
 
103
    def _assign_id(self):
 
104
        """Produce a new tranform id"""
 
105
        new_id = "new-%s" % self._id_number
 
106
        self._id_number +=1
 
107
        return new_id
 
108
 
 
109
    def create_path(self, name, parent):
 
110
        """Assign a transaction id to a new path"""
 
111
        trans_id = self._assign_id()
 
112
        unique_add(self._new_name, trans_id, name)
 
113
        unique_add(self._new_parent, trans_id, parent)
 
114
        return trans_id
 
115
 
 
116
    def adjust_path(self, name, parent, trans_id):
 
117
        """Change the path that is assigned to a transaction id."""
 
118
        if trans_id == self._new_root:
 
119
            raise CantMoveRoot
 
120
        self._new_name[trans_id] = name
 
121
        self._new_parent[trans_id] = parent
 
122
 
 
123
    def adjust_root_path(self, name, parent):
 
124
        """Emulate moving the root by moving all children, instead.
 
125
        
 
126
        We do this by undoing the association of root's transaction id with the
 
127
        current tree.  This allows us to create a new directory with that
 
128
        transaction id.  We unversion the root directory and version the 
 
129
        physically new directory, and hope someone versions the tree root
 
130
        later.
 
131
        """
 
132
        old_root = self._new_root
 
133
        old_root_file_id = self.final_file_id(old_root)
 
134
        # force moving all children of root
 
135
        for child_id in self.iter_tree_children(old_root):
 
136
            if child_id != parent:
 
137
                self.adjust_path(self.final_name(child_id), 
 
138
                                 self.final_parent(child_id), child_id)
 
139
            file_id = self.final_file_id(child_id)
 
140
            if file_id is not None:
 
141
                self.unversion_file(child_id)
 
142
            self.version_file(file_id, child_id)
 
143
        
 
144
        # the physical root needs a new transaction id
 
145
        self._tree_path_ids.pop("")
 
146
        self._tree_id_paths.pop(old_root)
 
147
        self._new_root = self.get_id_tree(self._tree.get_root_id())
 
148
        if parent == old_root:
 
149
            parent = self._new_root
 
150
        self.adjust_path(name, parent, old_root)
 
151
        self.create_directory(old_root)
 
152
        self.version_file(old_root_file_id, old_root)
 
153
        self.unversion_file(self._new_root)
 
154
 
 
155
    def get_id_tree(self, inventory_id):
 
156
        """Determine the transaction id of a working tree file.
 
157
        
 
158
        This reflects only files that already exist, not ones that will be
 
159
        added by transactions.
 
160
        """
 
161
        path = self._tree.inventory.id2path(inventory_id)
 
162
        return self.get_tree_path_id(path)
 
163
 
 
164
    def get_trans_id(self, file_id):
 
165
        """Determine or set the transaction id associated with a file ID.
 
166
        A new id is only created for file_ids that were never present.  If
 
167
        a transaction has been unversioned, it is deliberately still returned.
 
168
        (this will likely lead to an unversioned parent conflict.)
 
169
        """
 
170
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
 
171
            return self._r_new_id[file_id]
 
172
        elif file_id in self._tree.inventory:
 
173
            return self.get_id_tree(file_id)
 
174
        elif file_id in self._non_present_ids:
 
175
            return self._non_present_ids[file_id]
 
176
        else:
 
177
            trans_id = self._assign_id()
 
178
            self._non_present_ids[file_id] = trans_id
 
179
            return trans_id
 
180
 
 
181
    def canonical_path(self, path):
 
182
        """Get the canonical tree-relative path"""
 
183
        # don't follow final symlinks
 
184
        dirname, basename = os.path.split(self._tree.abspath(path))
 
185
        dirname = os.path.realpath(dirname)
 
186
        return self._tree.relpath(pathjoin(dirname, basename))
 
187
 
 
188
    def get_tree_path_id(self, path):
 
189
        """Determine (and maybe set) the transaction ID for a tree path."""
 
190
        path = self.canonical_path(path)
 
191
        if path not in self._tree_path_ids:
 
192
            self._tree_path_ids[path] = self._assign_id()
 
193
            self._tree_id_paths[self._tree_path_ids[path]] = path
 
194
        return self._tree_path_ids[path]
 
195
 
 
196
    def get_tree_parent(self, trans_id):
 
197
        """Determine id of the parent in the tree."""
 
198
        path = self._tree_id_paths[trans_id]
 
199
        if path == "":
 
200
            return ROOT_PARENT
 
201
        return self.get_tree_path_id(os.path.dirname(path))
 
202
 
 
203
    def create_file(self, contents, trans_id, mode_id=None):
 
204
        """Schedule creation of a new file.
 
205
 
 
206
        See also new_file.
 
207
        
 
208
        Contents is an iterator of strings, all of which will be written
 
209
        to the target destination.
 
210
 
 
211
        New file takes the permissions of any existing file with that id,
 
212
        unless mode_id is specified.
 
213
        """
 
214
        f = file(self._limbo_name(trans_id), 'wb')
 
215
        unique_add(self._new_contents, trans_id, 'file')
 
216
        for segment in contents:
 
217
            f.write(segment)
 
218
        f.close()
 
219
        self._set_mode(trans_id, mode_id, S_ISREG)
 
220
 
 
221
    def _set_mode(self, trans_id, mode_id, typefunc):
 
222
        """Set the mode of new file contents.
 
223
        The mode_id is the existing file to get the mode from (often the same
 
224
        as trans_id).  The operation is only performed if there's a mode match
 
225
        according to typefunc.
 
226
        """
 
227
        if mode_id is None:
 
228
            mode_id = trans_id
 
229
        try:
 
230
            old_path = self._tree_id_paths[mode_id]
 
231
        except KeyError:
 
232
            return
 
233
        try:
 
234
            mode = os.stat(old_path).st_mode
 
235
        except OSError, e:
 
236
            if e.errno == errno.ENOENT:
 
237
                return
 
238
            else:
 
239
                raise
 
240
        if typefunc(mode):
 
241
            os.chmod(self._limbo_name(trans_id), mode)
 
242
 
 
243
    def create_directory(self, trans_id):
 
244
        """Schedule creation of a new directory.
 
245
        
 
246
        See also new_directory.
 
247
        """
 
248
        os.mkdir(self._limbo_name(trans_id))
 
249
        unique_add(self._new_contents, trans_id, 'directory')
 
250
 
 
251
    def create_symlink(self, target, trans_id):
 
252
        """Schedule creation of a new symbolic link.
 
253
 
 
254
        target is a bytestring.
 
255
        See also new_symlink.
 
256
        """
 
257
        os.symlink(target, self._limbo_name(trans_id))
 
258
        unique_add(self._new_contents, trans_id, 'symlink')
 
259
 
 
260
    @staticmethod
 
261
    def delete_any(full_path):
 
262
        """Delete a file or directory."""
 
263
        try:
 
264
            os.unlink(full_path)
 
265
        except OSError, e:
 
266
        # We may be renaming a dangling inventory id
 
267
            if e.errno != errno.EISDIR and e.errno != errno.EACCES:
 
268
                raise
 
269
            os.rmdir(full_path)
 
270
 
 
271
    def cancel_creation(self, trans_id):
 
272
        """Cancel the creation of new file contents."""
 
273
        del self._new_contents[trans_id]
 
274
        self.delete_any(self._limbo_name(trans_id))
 
275
 
 
276
    def delete_contents(self, trans_id):
 
277
        """Schedule the contents of a path entry for deletion"""
 
278
        self.tree_kind(trans_id)
 
279
        self._removed_contents.add(trans_id)
 
280
 
 
281
    def cancel_deletion(self, trans_id):
 
282
        """Cancel a scheduled deletion"""
 
283
        self._removed_contents.remove(trans_id)
 
284
 
 
285
    def unversion_file(self, trans_id):
 
286
        """Schedule a path entry to become unversioned"""
 
287
        self._removed_id.add(trans_id)
 
288
 
 
289
    def delete_versioned(self, trans_id):
 
290
        """Delete and unversion a versioned file"""
 
291
        self.delete_contents(trans_id)
 
292
        self.unversion_file(trans_id)
 
293
 
 
294
    def set_executability(self, executability, trans_id):
 
295
        """Schedule setting of the 'execute' bit
 
296
        To unschedule, set to None
 
297
        """
 
298
        if executability is None:
 
299
            del self._new_executability[trans_id]
 
300
        else:
 
301
            unique_add(self._new_executability, trans_id, executability)
 
302
 
 
303
    def version_file(self, file_id, trans_id):
 
304
        """Schedule a file to become versioned."""
 
305
        assert file_id is not None
 
306
        unique_add(self._new_id, trans_id, file_id)
 
307
        unique_add(self._r_new_id, file_id, trans_id)
 
308
 
 
309
    def cancel_versioning(self, trans_id):
 
310
        """Undo a previous versioning of a file"""
 
311
        file_id = self._new_id[trans_id]
 
312
        del self._new_id[trans_id]
 
313
        del self._r_new_id[file_id]
 
314
 
 
315
    def new_paths(self):
 
316
        """Determine the paths of all new and changed files"""
 
317
        new_ids = set()
 
318
        fp = FinalPaths(self)
 
319
        for id_set in (self._new_name, self._new_parent, self._new_contents,
 
320
                       self._new_id, self._new_executability):
 
321
            new_ids.update(id_set)
 
322
        new_paths = [(fp.get_path(t), t) for t in new_ids]
 
323
        new_paths.sort()
 
324
        return new_paths
 
325
 
 
326
    def tree_kind(self, trans_id):
 
327
        """Determine the file kind in the working tree.
 
328
 
 
329
        Raises NoSuchFile if the file does not exist
 
330
        """
 
331
        path = self._tree_id_paths.get(trans_id)
 
332
        if path is None:
 
333
            raise NoSuchFile(None)
 
334
        try:
 
335
            return file_kind(self._tree.abspath(path))
 
336
        except OSError, e:
 
337
            if e.errno != errno.ENOENT:
 
338
                raise
 
339
            else:
 
340
                raise NoSuchFile(path)
 
341
 
 
342
    def final_kind(self, trans_id):
 
343
        """Determine the final file kind, after any changes applied.
 
344
        
 
345
        Raises NoSuchFile if the file does not exist/has no contents.
 
346
        (It is conceivable that a path would be created without the
 
347
        corresponding contents insertion command)
 
348
        """
 
349
        if trans_id in self._new_contents:
 
350
            return self._new_contents[trans_id]
 
351
        elif trans_id in self._removed_contents:
 
352
            raise NoSuchFile(None)
 
353
        else:
 
354
            return self.tree_kind(trans_id)
 
355
 
 
356
    def get_tree_file_id(self, trans_id):
 
357
        """Determine the file id associated with the trans_id in the tree"""
 
358
        try:
 
359
            path = self._tree_id_paths[trans_id]
 
360
        except KeyError:
 
361
            # the file is a new, unversioned file, or invalid trans_id
 
362
            return None
 
363
        # the file is old; the old id is still valid
 
364
        if self._new_root == trans_id:
 
365
            return self._tree.inventory.root.file_id
 
366
        return self._tree.inventory.path2id(path)
 
367
 
 
368
    def final_file_id(self, trans_id):
 
369
        """Determine the file id after any changes are applied, or None.
 
370
        
 
371
        None indicates that the file will not be versioned after changes are
 
372
        applied.
 
373
        """
 
374
        try:
 
375
            # there is a new id for this file
 
376
            assert self._new_id[trans_id] is not None
 
377
            return self._new_id[trans_id]
 
378
        except KeyError:
 
379
            if trans_id in self._removed_id:
 
380
                return None
 
381
        return self.get_tree_file_id(trans_id)
 
382
 
 
383
    def inactive_file_id(self, trans_id):
 
384
        """Return the inactive file_id associated with a transaction id.
 
385
        That is, the one in the tree or in non_present_ids.
 
386
        The file_id may actually be active, too.
 
387
        """
 
388
        file_id = self.get_tree_file_id(trans_id)
 
389
        if file_id is not None:
 
390
            return file_id
 
391
        for key, value in self._non_present_ids.iteritems():
 
392
            if value == trans_id:
 
393
                return key
 
394
 
 
395
    def final_parent(self, trans_id):
 
396
        """Determine the parent file_id, after any changes are applied.
 
397
 
 
398
        ROOT_PARENT is returned for the tree root.
 
399
        """
 
400
        try:
 
401
            return self._new_parent[trans_id]
 
402
        except KeyError:
 
403
            return self.get_tree_parent(trans_id)
 
404
 
 
405
    def final_name(self, trans_id):
 
406
        """Determine the final filename, after all changes are applied."""
 
407
        try:
 
408
            return self._new_name[trans_id]
 
409
        except KeyError:
 
410
            return os.path.basename(self._tree_id_paths[trans_id])
 
411
 
 
412
    def _by_parent(self):
 
413
        """Return a map of parent: children for known parents.
 
414
        
 
415
        Only new paths and parents of tree files with assigned ids are used.
 
416
        """
 
417
        by_parent = {}
 
418
        items = list(self._new_parent.iteritems())
 
419
        items.extend((t, self.final_parent(t)) for t in 
 
420
                      self._tree_id_paths.keys())
 
421
        for trans_id, parent_id in items:
 
422
            if parent_id not in by_parent:
 
423
                by_parent[parent_id] = set()
 
424
            by_parent[parent_id].add(trans_id)
 
425
        return by_parent
 
426
 
 
427
    def path_changed(self, trans_id):
 
428
        """Return True if a trans_id's path has changed."""
 
429
        return trans_id in self._new_name or trans_id in self._new_parent
 
430
 
 
431
    def find_conflicts(self):
 
432
        """Find any violations of inventory or filesystem invariants"""
 
433
        if self.__done is True:
 
434
            raise ReusingTransform()
 
435
        conflicts = []
 
436
        # ensure all children of all existent parents are known
 
437
        # all children of non-existent parents are known, by definition.
 
438
        self._add_tree_children()
 
439
        by_parent = self._by_parent()
 
440
        conflicts.extend(self._unversioned_parents(by_parent))
 
441
        conflicts.extend(self._parent_loops())
 
442
        conflicts.extend(self._duplicate_entries(by_parent))
 
443
        conflicts.extend(self._duplicate_ids())
 
444
        conflicts.extend(self._parent_type_conflicts(by_parent))
 
445
        conflicts.extend(self._improper_versioning())
 
446
        conflicts.extend(self._executability_conflicts())
 
447
        conflicts.extend(self._overwrite_conflicts())
 
448
        return conflicts
 
449
 
 
450
    def _add_tree_children(self):
 
451
        """Add all the children of all active parents to the known paths.
 
452
 
 
453
        Active parents are those which gain children, and those which are
 
454
        removed.  This is a necessary first step in detecting conflicts.
 
455
        """
 
456
        parents = self._by_parent().keys()
 
457
        parents.extend([t for t in self._removed_contents if 
 
458
                        self.tree_kind(t) == 'directory'])
 
459
        for trans_id in self._removed_id:
 
460
            file_id = self.get_tree_file_id(trans_id)
 
461
            if self._tree.inventory[file_id].kind in ('directory', 
 
462
                                                      'root_directory'):
 
463
                parents.append(trans_id)
 
464
 
 
465
        for parent_id in parents:
 
466
            # ensure that all children are registered with the transaction
 
467
            list(self.iter_tree_children(parent_id))
 
468
 
 
469
    def iter_tree_children(self, parent_id):
 
470
        """Iterate through the entry's tree children, if any"""
 
471
        try:
 
472
            path = self._tree_id_paths[parent_id]
 
473
        except KeyError:
 
474
            return
 
475
        try:
 
476
            children = os.listdir(self._tree.abspath(path))
 
477
        except OSError, e:
 
478
            if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
 
479
                raise
 
480
            return
 
481
            
 
482
        for child in children:
 
483
            childpath = joinpath(path, child)
 
484
            if childpath == BZRDIR:
 
485
                continue
 
486
            yield self.get_tree_path_id(childpath)
 
487
 
 
488
    def _parent_loops(self):
 
489
        """No entry should be its own ancestor"""
 
490
        conflicts = []
 
491
        for trans_id in self._new_parent:
 
492
            seen = set()
 
493
            parent_id = trans_id
 
494
            while parent_id is not ROOT_PARENT:
 
495
                seen.add(parent_id)
 
496
                parent_id = self.final_parent(parent_id)
 
497
                if parent_id == trans_id:
 
498
                    conflicts.append(('parent loop', trans_id))
 
499
                if parent_id in seen:
 
500
                    break
 
501
        return conflicts
 
502
 
 
503
    def _unversioned_parents(self, by_parent):
 
504
        """If parent directories are versioned, children must be versioned."""
 
505
        conflicts = []
 
506
        for parent_id, children in by_parent.iteritems():
 
507
            if parent_id is ROOT_PARENT:
 
508
                continue
 
509
            if self.final_file_id(parent_id) is not None:
 
510
                continue
 
511
            for child_id in children:
 
512
                if self.final_file_id(child_id) is not None:
 
513
                    conflicts.append(('unversioned parent', parent_id))
 
514
                    break;
 
515
        return conflicts
 
516
 
 
517
    def _improper_versioning(self):
 
518
        """Cannot version a file with no contents, or a bad type.
 
519
        
 
520
        However, existing entries with no contents are okay.
 
521
        """
 
522
        conflicts = []
 
523
        for trans_id in self._new_id.iterkeys():
 
524
            try:
 
525
                kind = self.final_kind(trans_id)
 
526
            except NoSuchFile:
 
527
                conflicts.append(('versioning no contents', trans_id))
 
528
                continue
 
529
            if not InventoryEntry.versionable_kind(kind):
 
530
                conflicts.append(('versioning bad kind', trans_id, kind))
 
531
        return conflicts
 
532
 
 
533
    def _executability_conflicts(self):
 
534
        """Check for bad executability changes.
 
535
        
 
536
        Only versioned files may have their executability set, because
 
537
        1. only versioned entries can have executability under windows
 
538
        2. only files can be executable.  (The execute bit on a directory
 
539
           does not indicate searchability)
 
540
        """
 
541
        conflicts = []
 
542
        for trans_id in self._new_executability:
 
543
            if self.final_file_id(trans_id) is None:
 
544
                conflicts.append(('unversioned executability', trans_id))
 
545
            else:
 
546
                try:
 
547
                    non_file = self.final_kind(trans_id) != "file"
 
548
                except NoSuchFile:
 
549
                    non_file = True
 
550
                if non_file is True:
 
551
                    conflicts.append(('non-file executability', trans_id))
 
552
        return conflicts
 
553
 
 
554
    def _overwrite_conflicts(self):
 
555
        """Check for overwrites (not permitted on Win32)"""
 
556
        conflicts = []
 
557
        for trans_id in self._new_contents:
 
558
            try:
 
559
                self.tree_kind(trans_id)
 
560
            except NoSuchFile:
 
561
                continue
 
562
            if trans_id not in self._removed_contents:
 
563
                conflicts.append(('overwrite', trans_id,
 
564
                                 self.final_name(trans_id)))
 
565
        return conflicts
 
566
 
 
567
    def _duplicate_entries(self, by_parent):
 
568
        """No directory may have two entries with the same name."""
 
569
        conflicts = []
 
570
        for children in by_parent.itervalues():
 
571
            name_ids = [(self.final_name(t), t) for t in children]
 
572
            name_ids.sort()
 
573
            last_name = None
 
574
            last_trans_id = None
 
575
            for name, trans_id in name_ids:
 
576
                if name == last_name:
 
577
                    conflicts.append(('duplicate', last_trans_id, trans_id,
 
578
                    name))
 
579
                last_name = name
 
580
                last_trans_id = trans_id
 
581
        return conflicts
 
582
 
 
583
    def _duplicate_ids(self):
 
584
        """Each inventory id may only be used once"""
 
585
        conflicts = []
 
586
        removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
 
587
                                self._removed_id))
 
588
        active_tree_ids = set((f for f in self._tree.inventory if
 
589
                               f not in removed_tree_ids))
 
590
        for trans_id, file_id in self._new_id.iteritems():
 
591
            if file_id in active_tree_ids:
 
592
                old_trans_id = self.get_id_tree(file_id)
 
593
                conflicts.append(('duplicate id', old_trans_id, trans_id))
 
594
        return conflicts
 
595
 
 
596
    def _parent_type_conflicts(self, by_parent):
 
597
        """parents must have directory 'contents'."""
 
598
        conflicts = []
 
599
        for parent_id, children in by_parent.iteritems():
 
600
            if parent_id is ROOT_PARENT:
 
601
                continue
 
602
            if not self._any_contents(children):
 
603
                continue
 
604
            for child in children:
 
605
                try:
 
606
                    self.final_kind(child)
 
607
                except NoSuchFile:
 
608
                    continue
 
609
            try:
 
610
                kind = self.final_kind(parent_id)
 
611
            except NoSuchFile:
 
612
                kind = None
 
613
            if kind is None:
 
614
                conflicts.append(('missing parent', parent_id))
 
615
            elif kind != "directory":
 
616
                conflicts.append(('non-directory parent', parent_id))
 
617
        return conflicts
 
618
 
 
619
    def _any_contents(self, trans_ids):
 
620
        """Return true if any of the trans_ids, will have contents."""
 
621
        for trans_id in trans_ids:
 
622
            try:
 
623
                kind = self.final_kind(trans_id)
 
624
            except NoSuchFile:
 
625
                continue
 
626
            return True
 
627
        return False
 
628
            
 
629
    def apply(self):
 
630
        """Apply all changes to the inventory and filesystem.
 
631
        
 
632
        If filesystem or inventory conflicts are present, MalformedTransform
 
633
        will be thrown.
 
634
        """
 
635
        conflicts = self.find_conflicts()
 
636
        if len(conflicts) != 0:
 
637
            raise MalformedTransform(conflicts=conflicts)
 
638
        limbo_inv = {}
 
639
        inv = self._tree.inventory
 
640
        self._apply_removals(inv, limbo_inv)
 
641
        self._apply_insertions(inv, limbo_inv)
 
642
        self._tree._write_inventory(inv)
 
643
        self.__done = True
 
644
        self.finalize()
 
645
 
 
646
    def _limbo_name(self, trans_id):
 
647
        """Generate the limbo name of a file"""
 
648
        return pathjoin(self._limbodir, trans_id)
 
649
 
 
650
    def _apply_removals(self, inv, limbo_inv):
 
651
        """Perform tree operations that remove directory/inventory names.
 
652
        
 
653
        That is, delete files that are to be deleted, and put any files that
 
654
        need renaming into limbo.  This must be done in strict child-to-parent
 
655
        order.
 
656
        """
 
657
        tree_paths = list(self._tree_path_ids.iteritems())
 
658
        tree_paths.sort(reverse=True)
 
659
        for num, data in enumerate(tree_paths):
 
660
            path, trans_id = data
 
661
            self._pb.update('removing file', num+1, len(tree_paths))
 
662
            full_path = self._tree.abspath(path)
 
663
            if trans_id in self._removed_contents:
 
664
                self.delete_any(full_path)
 
665
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
666
                try:
 
667
                    os.rename(full_path, self._limbo_name(trans_id))
 
668
                except OSError, e:
 
669
                    if e.errno != errno.ENOENT:
 
670
                        raise
 
671
            if trans_id in self._removed_id:
 
672
                if trans_id == self._new_root:
 
673
                    file_id = self._tree.inventory.root.file_id
 
674
                else:
 
675
                    file_id = self.get_tree_file_id(trans_id)
 
676
                del inv[file_id]
 
677
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
678
                file_id = self.get_tree_file_id(trans_id)
 
679
                if file_id is not None:
 
680
                    limbo_inv[trans_id] = inv[file_id]
 
681
                    del inv[file_id]
 
682
        self._pb.clear()
 
683
 
 
684
    def _apply_insertions(self, inv, limbo_inv):
 
685
        """Perform tree operations that insert directory/inventory names.
 
686
        
 
687
        That is, create any files that need to be created, and restore from
 
688
        limbo any files that needed renaming.  This must be done in strict
 
689
        parent-to-child order.
 
690
        """
 
691
        new_paths = self.new_paths()
 
692
        for num, (path, trans_id) in enumerate(new_paths):
 
693
            self._pb.update('adding file', num+1, len(new_paths))
 
694
            try:
 
695
                kind = self._new_contents[trans_id]
 
696
            except KeyError:
 
697
                kind = contents = None
 
698
            if trans_id in self._new_contents or self.path_changed(trans_id):
 
699
                full_path = self._tree.abspath(path)
 
700
                try:
 
701
                    os.rename(self._limbo_name(trans_id), full_path)
 
702
                except OSError, e:
 
703
                    # We may be renaming a dangling inventory id
 
704
                    if e.errno != errno.ENOENT:
 
705
                        raise
 
706
                if trans_id in self._new_contents:
 
707
                    del self._new_contents[trans_id]
 
708
 
 
709
            if trans_id in self._new_id:
 
710
                if kind is None:
 
711
                    kind = file_kind(self._tree.abspath(path))
 
712
                inv.add_path(path, kind, self._new_id[trans_id])
 
713
            elif trans_id in self._new_name or trans_id in self._new_parent:
 
714
                entry = limbo_inv.get(trans_id)
 
715
                if entry is not None:
 
716
                    entry.name = self.final_name(trans_id)
 
717
                    parent_path = os.path.dirname(path)
 
718
                    entry.parent_id = self._tree.inventory.path2id(parent_path)
 
719
                    inv.add(entry)
 
720
 
 
721
            # requires files and inventory entries to be in place
 
722
            if trans_id in self._new_executability:
 
723
                self._set_executability(path, inv, trans_id)
 
724
        self._pb.clear()
 
725
 
 
726
    def _set_executability(self, path, inv, trans_id):
 
727
        """Set the executability of versioned files """
 
728
        file_id = inv.path2id(path)
 
729
        new_executability = self._new_executability[trans_id]
 
730
        inv[file_id].executable = new_executability
 
731
        if supports_executable():
 
732
            abspath = self._tree.abspath(path)
 
733
            current_mode = os.stat(abspath).st_mode
 
734
            if new_executability:
 
735
                umask = os.umask(0)
 
736
                os.umask(umask)
 
737
                to_mode = current_mode | (0100 & ~umask)
 
738
                # Enable x-bit for others only if they can read it.
 
739
                if current_mode & 0004:
 
740
                    to_mode |= 0001 & ~umask
 
741
                if current_mode & 0040:
 
742
                    to_mode |= 0010 & ~umask
 
743
            else:
 
744
                to_mode = current_mode & ~0111
 
745
            os.chmod(abspath, to_mode)
 
746
 
 
747
    def _new_entry(self, name, parent_id, file_id):
 
748
        """Helper function to create a new filesystem entry."""
 
749
        trans_id = self.create_path(name, parent_id)
 
750
        if file_id is not None:
 
751
            self.version_file(file_id, trans_id)
 
752
        return trans_id
 
753
 
 
754
    def new_file(self, name, parent_id, contents, file_id=None, 
 
755
                 executable=None):
 
756
        """Convenience method to create files.
 
757
        
 
758
        name is the name of the file to create.
 
759
        parent_id is the transaction id of the parent directory of the file.
 
760
        contents is an iterator of bytestrings, which will be used to produce
 
761
        the file.
 
762
        file_id is the inventory ID of the file, if it is to be versioned.
 
763
        """
 
764
        trans_id = self._new_entry(name, parent_id, file_id)
 
765
        self.create_file(contents, trans_id)
 
766
        if executable is not None:
 
767
            self.set_executability(executable, trans_id)
 
768
        return trans_id
 
769
 
 
770
    def new_directory(self, name, parent_id, file_id=None):
 
771
        """Convenience method to create directories.
 
772
 
 
773
        name is the name of the directory to create.
 
774
        parent_id is the transaction id of the parent directory of the
 
775
        directory.
 
776
        file_id is the inventory ID of the directory, if it is to be versioned.
 
777
        """
 
778
        trans_id = self._new_entry(name, parent_id, file_id)
 
779
        self.create_directory(trans_id)
 
780
        return trans_id 
 
781
 
 
782
    def new_symlink(self, name, parent_id, target, file_id=None):
 
783
        """Convenience method to create symbolic link.
 
784
        
 
785
        name is the name of the symlink to create.
 
786
        parent_id is the transaction id of the parent directory of the symlink.
 
787
        target is a bytestring of the target of the symlink.
 
788
        file_id is the inventory ID of the file, if it is to be versioned.
 
789
        """
 
790
        trans_id = self._new_entry(name, parent_id, file_id)
 
791
        self.create_symlink(target, trans_id)
 
792
        return trans_id
 
793
 
 
794
def joinpath(parent, child):
 
795
    """Join tree-relative paths, handling the tree root specially"""
 
796
    if parent is None or parent == "":
 
797
        return child
 
798
    else:
 
799
        return pathjoin(parent, child)
 
800
 
 
801
 
 
802
class FinalPaths(object):
 
803
    """Make path calculation cheap by memoizing paths.
 
804
 
 
805
    The underlying tree must not be manipulated between calls, or else
 
806
    the results will likely be incorrect.
 
807
    """
 
808
    def __init__(self, transform):
 
809
        object.__init__(self)
 
810
        self._known_paths = {}
 
811
        self.transform = transform
 
812
 
 
813
    def _determine_path(self, trans_id):
 
814
        if trans_id == self.transform.root:
 
815
            return ""
 
816
        name = self.transform.final_name(trans_id)
 
817
        parent_id = self.transform.final_parent(trans_id)
 
818
        if parent_id == self.transform.root:
 
819
            return name
 
820
        else:
 
821
            return pathjoin(self.get_path(parent_id), name)
 
822
 
 
823
    def get_path(self, trans_id):
 
824
        """Find the final path associated with a trans_id"""
 
825
        if trans_id not in self._known_paths:
 
826
            self._known_paths[trans_id] = self._determine_path(trans_id)
 
827
        return self._known_paths[trans_id]
 
828
 
 
829
def topology_sorted_ids(tree):
 
830
    """Determine the topological order of the ids in a tree"""
 
831
    file_ids = list(tree)
 
832
    file_ids.sort(key=tree.id2path)
 
833
    return file_ids
 
834
 
 
835
def build_tree(tree, wt):
 
836
    """Create working tree for a branch, using a Transaction."""
 
837
    file_trans_id = {}
 
838
    tt = TreeTransform(wt)
 
839
    try:
 
840
        file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
 
841
        file_ids = topology_sorted_ids(tree)
 
842
        for file_id in file_ids:
 
843
            entry = tree.inventory[file_id]
 
844
            if entry.parent_id is None:
 
845
                continue
 
846
            if entry.parent_id not in file_trans_id:
 
847
                raise repr(entry.parent_id)
 
848
            parent_id = file_trans_id[entry.parent_id]
 
849
            file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
 
850
        tt.apply()
 
851
    finally:
 
852
        tt.finalize()
 
853
 
 
854
def new_by_entry(tt, entry, parent_id, tree):
 
855
    """Create a new file according to its inventory entry"""
 
856
    name = entry.name
 
857
    kind = entry.kind
 
858
    if kind == 'file':
 
859
        contents = tree.get_file(entry.file_id).readlines()
 
860
        executable = tree.is_executable(entry.file_id)
 
861
        return tt.new_file(name, parent_id, contents, entry.file_id, 
 
862
                           executable)
 
863
    elif kind == 'directory':
 
864
        return tt.new_directory(name, parent_id, entry.file_id)
 
865
    elif kind == 'symlink':
 
866
        target = entry.get_symlink_target(file_id)
 
867
        return tt.new_symlink(name, parent_id, target, file_id)
 
868
 
 
869
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
 
870
    """Create new file contents according to an inventory entry."""
 
871
    if entry.kind == "file":
 
872
        if lines == None:
 
873
            lines = tree.get_file(entry.file_id).readlines()
 
874
        tt.create_file(lines, trans_id, mode_id=mode_id)
 
875
    elif entry.kind == "symlink":
 
876
        tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
 
877
    elif entry.kind == "directory":
 
878
        tt.create_directory(trans_id)
 
879
 
 
880
def create_entry_executability(tt, entry, trans_id):
 
881
    """Set the executability of a trans_id according to an inventory entry"""
 
882
    if entry.kind == "file":
 
883
        tt.set_executability(entry.executable, trans_id)
 
884
 
 
885
 
 
886
def find_interesting(working_tree, target_tree, filenames):
 
887
    """Find the ids corresponding to specified filenames."""
 
888
    if not filenames:
 
889
        interesting_ids = None
 
890
    else:
 
891
        interesting_ids = set()
 
892
        for tree_path in filenames:
 
893
            for tree in (working_tree, target_tree):
 
894
                not_found = True
 
895
                file_id = tree.inventory.path2id(tree_path)
 
896
                if file_id is not None:
 
897
                    interesting_ids.add(file_id)
 
898
                    not_found = False
 
899
                if not_found:
 
900
                    raise NotVersionedError(path=tree_path)
 
901
    return interesting_ids
 
902
 
 
903
 
 
904
def change_entry(tt, file_id, working_tree, target_tree, 
 
905
                 get_trans_id, backups, trans_id):
 
906
    """Replace a file_id's contents with those from a target tree."""
 
907
    e_trans_id = get_trans_id(file_id)
 
908
    entry = target_tree.inventory[file_id]
 
909
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
 
910
                                                           working_tree)
 
911
    if contents_mod:
 
912
        mode_id = e_trans_id
 
913
        if has_contents:
 
914
            if not backups:
 
915
                tt.delete_contents(e_trans_id)
 
916
            else:
 
917
                parent_trans_id = get_trans_id(entry.parent_id)
 
918
                tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
 
919
                tt.unversion_file(e_trans_id)
 
920
                e_trans_id = tt.create_path(entry.name, parent_trans_id)
 
921
                tt.version_file(file_id, e_trans_id)
 
922
                trans_id[file_id] = e_trans_id
 
923
        create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
 
924
        create_entry_executability(tt, entry, e_trans_id)
 
925
 
 
926
    elif meta_mod:
 
927
        tt.set_executability(entry.executable, e_trans_id)
 
928
    if tt.final_name(e_trans_id) != entry.name:
 
929
        adjust_path  = True
 
930
    else:
 
931
        parent_id = tt.final_parent(e_trans_id)
 
932
        parent_file_id = tt.final_file_id(parent_id)
 
933
        if parent_file_id != entry.parent_id:
 
934
            adjust_path = True
 
935
        else:
 
936
            adjust_path = False
 
937
    if adjust_path:
 
938
        parent_trans_id = get_trans_id(entry.parent_id)
 
939
        tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
 
940
 
 
941
 
 
942
def _entry_changes(file_id, entry, working_tree):
 
943
    """Determine in which ways the inventory entry has changed.
 
944
 
 
945
    Returns booleans: has_contents, content_mod, meta_mod
 
946
    has_contents means there are currently contents, but they differ
 
947
    contents_mod means contents need to be modified
 
948
    meta_mod means the metadata needs to be modified
 
949
    """
 
950
    cur_entry = working_tree.inventory[file_id]
 
951
    try:
 
952
        working_kind = working_tree.kind(file_id)
 
953
        has_contents = True
 
954
    except OSError, e:
 
955
        if e.errno != errno.ENOENT:
 
956
            raise
 
957
        has_contents = False
 
958
        contents_mod = True
 
959
        meta_mod = False
 
960
    if has_contents is True:
 
961
        real_e_kind = entry.kind
 
962
        if real_e_kind == 'root_directory':
 
963
            real_e_kind = 'directory'
 
964
        if real_e_kind != working_kind:
 
965
            contents_mod, meta_mod = True, False
 
966
        else:
 
967
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
 
968
                                       working_tree)
 
969
            contents_mod, meta_mod = entry.detect_changes(cur_entry)
 
970
    return has_contents, contents_mod, meta_mod
 
971
 
 
972
 
 
973
def revert(working_tree, target_tree, filenames, backups=False):
 
974
    """Revert a working tree's contents to those of a target tree."""
 
975
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
 
976
    def interesting(file_id):
 
977
        return interesting_ids is None or file_id in interesting_ids
 
978
 
 
979
    tt = TreeTransform(working_tree)
 
980
    try:
 
981
        trans_id = {}
 
982
        def get_trans_id(file_id):
 
983
            try:
 
984
                return trans_id[file_id]
 
985
            except KeyError:
 
986
                return tt.get_id_tree(file_id)
 
987
 
 
988
        for file_id in topology_sorted_ids(target_tree):
 
989
            if not interesting(file_id):
 
990
                continue
 
991
            if file_id not in working_tree.inventory:
 
992
                entry = target_tree.inventory[file_id]
 
993
                parent_id = get_trans_id(entry.parent_id)
 
994
                e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
 
995
                trans_id[file_id] = e_trans_id
 
996
            else:
 
997
                change_entry(tt, file_id, working_tree, target_tree, 
 
998
                             get_trans_id, backups, trans_id)
 
999
        for file_id in working_tree:
 
1000
            if not interesting(file_id):
 
1001
                continue
 
1002
            if file_id not in target_tree:
 
1003
                tt.unversion_file(tt.get_id_tree(file_id))
 
1004
        conflicts = resolve_conflicts(tt)
 
1005
        tt.apply()
 
1006
    finally:
 
1007
        tt.finalize()
 
1008
 
 
1009
 
 
1010
def resolve_conflicts(tt, pb=DummyProgress()):
 
1011
    """Make many conflict-resolution attempts, but die if they fail"""
 
1012
    new_conflicts = set()
 
1013
    try:
 
1014
        for n in range(10):
 
1015
            pb.update('Resolution pass', n+1, 10)
 
1016
            conflicts = tt.find_conflicts()
 
1017
            if len(conflicts) == 0:
 
1018
                return new_conflicts
 
1019
            new_conflicts.update(conflict_pass(tt, conflicts))
 
1020
        raise MalformedTransform(conflicts=conflicts)
 
1021
    finally:
 
1022
        pb.clear()
 
1023
 
 
1024
 
 
1025
def conflict_pass(tt, conflicts):
 
1026
    """Resolve some classes of conflicts."""
 
1027
    new_conflicts = set()
 
1028
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
1029
        if c_type == 'duplicate id':
 
1030
            tt.unversion_file(conflict[1])
 
1031
            new_conflicts.add((c_type, 'Unversioned existing file',
 
1032
                               conflict[1], conflict[2], ))
 
1033
        elif c_type == 'duplicate':
 
1034
            # files that were renamed take precedence
 
1035
            new_name = tt.final_name(conflict[1])+'.moved'
 
1036
            final_parent = tt.final_parent(conflict[1])
 
1037
            if tt.path_changed(conflict[1]):
 
1038
                tt.adjust_path(new_name, final_parent, conflict[2])
 
1039
                new_conflicts.add((c_type, 'Moved existing file to', 
 
1040
                                   conflict[2], conflict[1]))
 
1041
            else:
 
1042
                tt.adjust_path(new_name, final_parent, conflict[1])
 
1043
                new_conflicts.add((c_type, 'Moved existing file to', 
 
1044
                                  conflict[1], conflict[2]))
 
1045
        elif c_type == 'parent loop':
 
1046
            # break the loop by undoing one of the ops that caused the loop
 
1047
            cur = conflict[1]
 
1048
            while not tt.path_changed(cur):
 
1049
                cur = tt.final_parent(cur)
 
1050
            new_conflicts.add((c_type, 'Cancelled move', cur,
 
1051
                               tt.final_parent(cur),))
 
1052
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
1053
            
 
1054
        elif c_type == 'missing parent':
 
1055
            trans_id = conflict[1]
 
1056
            try:
 
1057
                tt.cancel_deletion(trans_id)
 
1058
                new_conflicts.add((c_type, 'Not deleting', trans_id))
 
1059
            except KeyError:
 
1060
                tt.create_directory(trans_id)
 
1061
                new_conflicts.add((c_type, 'Created directory.', trans_id))
 
1062
        elif c_type == 'unversioned parent':
 
1063
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
 
1064
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
 
1065
    return new_conflicts
 
1066
 
 
1067
def cook_conflicts(raw_conflicts, tt):
 
1068
    """Generate a list of cooked conflicts, sorted by file path"""
 
1069
    def key(conflict):
 
1070
        if conflict[2] is not None:
 
1071
            return conflict[2], conflict[0]
 
1072
        elif len(conflict) == 6:
 
1073
            return conflict[4], conflict[0]
 
1074
        else:
 
1075
            return None, conflict[0]
 
1076
 
 
1077
    return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
 
1078
 
 
1079
def iter_cook_conflicts(raw_conflicts, tt):
 
1080
    cooked_conflicts = []
 
1081
    fp = FinalPaths(tt)
 
1082
    for conflict in raw_conflicts:
 
1083
        c_type = conflict[0]
 
1084
        action = conflict[1]
 
1085
        modified_path = fp.get_path(conflict[2])
 
1086
        modified_id = tt.final_file_id(conflict[2])
 
1087
        if len(conflict) == 3:
 
1088
            yield c_type, action, modified_path, modified_id
 
1089
        else:
 
1090
            conflicting_path = fp.get_path(conflict[3])
 
1091
            conflicting_id = tt.final_file_id(conflict[3])
 
1092
            yield (c_type, action, modified_path, modified_id, 
 
1093
                   conflicting_path, conflicting_id)
 
1094
 
 
1095
 
 
1096
def conflicts_strings(conflicts):
 
1097
    """Generate strings for the provided conflicts"""
 
1098
    for conflict in conflicts:
 
1099
        conflict_type = conflict[0]
 
1100
        if conflict_type == 'text conflict':
 
1101
            yield 'Text conflict in %s' % conflict[2]
 
1102
        elif conflict_type == 'contents conflict':
 
1103
            yield 'Contents conflict in %s' % conflict[2]
 
1104
        elif conflict_type == 'path conflict':
 
1105
            yield 'Path conflict: %s / %s' % conflict[2:]
 
1106
        elif conflict_type == 'duplicate id':
 
1107
            vals = (conflict[4], conflict[1], conflict[2])
 
1108
            yield 'Conflict adding id to %s.  %s %s.' % vals
 
1109
        elif conflict_type == 'duplicate':
 
1110
            vals = (conflict[4], conflict[1], conflict[2])
 
1111
            yield 'Conflict adding file %s.  %s %s.' % vals
 
1112
        elif conflict_type == 'parent loop':
 
1113
            vals = (conflict[4], conflict[2], conflict[1])
 
1114
            yield 'Conflict moving %s into %s.  %s.' % vals
 
1115
        elif conflict_type == 'unversioned parent':
 
1116
            vals = (conflict[2], conflict[1])
 
1117
            yield 'Conflict adding versioned files to %s.  %s.' % vals
 
1118
        elif conflict_type == 'missing parent':
 
1119
            vals = (conflict[2], conflict[1])
 
1120
            yield 'Conflict adding files to %s.  %s.' % vals