~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-05-28 08:53:40 UTC
  • mfrom: (4354.5.4 disk-transform)
  • Revision ID: pqm@pqm.ubuntu.com-20090528085340-bfw8729wfm9kmfmd
(abentley) Extract BaseTreeTransform disk code into DiskTreeTransform.

Show diffs side-by-side

added added

removed removed

Lines of Context:
76
76
 
77
77
 
78
78
class TreeTransformBase(object):
79
 
    """The base class for TreeTransform and TreeTransformBase"""
 
79
    """The base class for TreeTransform and its kin."""
80
80
 
81
 
    def __init__(self, tree, limbodir, pb=DummyProgress(),
 
81
    def __init__(self, tree, pb=DummyProgress(),
82
82
                 case_sensitive=True):
83
83
        """Constructor.
84
84
 
85
85
        :param tree: The tree that will be transformed, but not necessarily
86
86
            the output tree.
87
 
        :param limbodir: A directory where new files can be stored until
88
 
            they are installed in their proper places
89
87
        :param pb: A ProgressBar indicating how much progress is being made
90
88
        :param case_sensitive: If True, the target of the transform is
91
89
            case sensitive, not just case preserving.
92
90
        """
93
91
        object.__init__(self)
94
92
        self._tree = tree
95
 
        self._limbodir = limbodir
96
 
        self._deletiondir = None
97
93
        self._id_number = 0
98
94
        # mapping of trans_id -> new basename
99
95
        self._new_name = {}
101
97
        self._new_parent = {}
102
98
        # mapping of trans_id with new contents -> new file_kind
103
99
        self._new_contents = {}
104
 
        # A mapping of transform ids to their limbo filename
105
 
        self._limbo_files = {}
106
 
        # A mapping of transform ids to a set of the transform ids of children
107
 
        # that their limbo directory has
108
 
        self._limbo_children = {}
109
 
        # Map transform ids to maps of child filename to child transform id
110
 
        self._limbo_children_names = {}
111
 
        # List of transform ids that need to be renamed from limbo into place
112
 
        self._needs_rename = set()
113
100
        # Set of trans_ids whose contents will be removed
114
101
        self._removed_contents = set()
115
102
        # Mapping of trans_id -> new execute-bit value
128
115
        self._tree_path_ids = {}
129
116
        # Mapping trans_id -> path in old tree
130
117
        self._tree_id_paths = {}
131
 
        # Cache of realpath results, to speed up canonical_path
132
 
        self._realpaths = {}
133
 
        # Cache of relpath results, to speed up canonical_path
134
 
        self._relpaths = {}
135
118
        # The trans_id that will be used as the tree root
136
119
        root_id = tree.get_root_id()
137
120
        if root_id is not None:
147
130
        # A counter of how many files have been renamed
148
131
        self.rename_count = 0
149
132
 
 
133
    def finalize(self):
 
134
        """Release the working tree lock, if held.
 
135
 
 
136
        This is required if apply has not been invoked, but can be invoked
 
137
        even after apply.
 
138
        """
 
139
        if self._tree is None:
 
140
            return
 
141
        self._tree.unlock()
 
142
        self._tree = None
 
143
 
150
144
    def __get_root(self):
151
145
        return self._new_root
152
146
 
153
147
    root = property(__get_root)
154
148
 
155
 
    def finalize(self):
156
 
        """Release the working tree lock, if held, clean up limbo dir.
157
 
 
158
 
        This is required if apply has not been invoked, but can be invoked
159
 
        even after apply.
160
 
        """
161
 
        if self._tree is None:
162
 
            return
163
 
        try:
164
 
            entries = [(self._limbo_name(t), t, k) for t, k in
165
 
                       self._new_contents.iteritems()]
166
 
            entries.sort(reverse=True)
167
 
            for path, trans_id, kind in entries:
168
 
                if kind == "directory":
169
 
                    os.rmdir(path)
170
 
                else:
171
 
                    os.unlink(path)
172
 
            try:
173
 
                os.rmdir(self._limbodir)
174
 
            except OSError:
175
 
                # We don't especially care *why* the dir is immortal.
176
 
                raise ImmortalLimbo(self._limbodir)
177
 
            try:
178
 
                if self._deletiondir is not None:
179
 
                    os.rmdir(self._deletiondir)
180
 
            except OSError:
181
 
                raise errors.ImmortalPendingDeletion(self._deletiondir)
182
 
        finally:
183
 
            self._tree.unlock()
184
 
            self._tree = None
185
 
 
186
149
    def _assign_id(self):
187
150
        """Produce a new tranform id"""
188
151
        new_id = "new-%s" % self._id_number
200
163
        """Change the path that is assigned to a transaction id."""
201
164
        if trans_id == self._new_root:
202
165
            raise CantMoveRoot
203
 
        previous_parent = self._new_parent.get(trans_id)
204
 
        previous_name = self._new_name.get(trans_id)
205
166
        self._new_name[trans_id] = name
206
167
        self._new_parent[trans_id] = parent
207
168
        if parent == ROOT_PARENT:
208
169
            if self._new_root is not None:
209
170
                raise ValueError("Cannot have multiple roots.")
210
171
            self._new_root = trans_id
211
 
        if (trans_id in self._limbo_files and
212
 
            trans_id not in self._needs_rename):
213
 
            self._rename_in_limbo([trans_id])
214
 
            self._limbo_children[previous_parent].remove(trans_id)
215
 
            del self._limbo_children_names[previous_parent][previous_name]
216
 
 
217
 
    def _rename_in_limbo(self, trans_ids):
218
 
        """Fix limbo names so that the right final path is produced.
219
 
 
220
 
        This means we outsmarted ourselves-- we tried to avoid renaming
221
 
        these files later by creating them with their final names in their
222
 
        final parents.  But now the previous name or parent is no longer
223
 
        suitable, so we have to rename them.
224
 
 
225
 
        Even for trans_ids that have no new contents, we must remove their
226
 
        entries from _limbo_files, because they are now stale.
227
 
        """
228
 
        for trans_id in trans_ids:
229
 
            old_path = self._limbo_files.pop(trans_id)
230
 
            if trans_id not in self._new_contents:
231
 
                continue
232
 
            new_path = self._limbo_name(trans_id)
233
 
            os.rename(old_path, new_path)
234
172
 
235
173
    def adjust_root_path(self, name, parent):
236
174
        """Emulate moving the root by moving all children, instead.
298
236
            else:
299
237
                return self.trans_id_tree_file_id(file_id)
300
238
 
301
 
    def canonical_path(self, path):
302
 
        """Get the canonical tree-relative path"""
303
 
        # don't follow final symlinks
304
 
        abs = self._tree.abspath(path)
305
 
        if abs in self._relpaths:
306
 
            return self._relpaths[abs]
307
 
        dirname, basename = os.path.split(abs)
308
 
        if dirname not in self._realpaths:
309
 
            self._realpaths[dirname] = os.path.realpath(dirname)
310
 
        dirname = self._realpaths[dirname]
311
 
        abs = pathjoin(dirname, basename)
312
 
        if dirname in self._relpaths:
313
 
            relpath = pathjoin(self._relpaths[dirname], basename)
314
 
            relpath = relpath.rstrip('/\\')
315
 
        else:
316
 
            relpath = self._tree.relpath(abs)
317
 
        self._relpaths[abs] = relpath
318
 
        return relpath
319
 
 
320
239
    def trans_id_tree_path(self, path):
321
240
        """Determine (and maybe set) the transaction ID for a tree path."""
322
241
        path = self.canonical_path(path)
332
251
            return ROOT_PARENT
333
252
        return self.trans_id_tree_path(os.path.dirname(path))
334
253
 
335
 
    def create_file(self, contents, trans_id, mode_id=None):
336
 
        """Schedule creation of a new file.
337
 
 
338
 
        See also new_file.
339
 
 
340
 
        Contents is an iterator of strings, all of which will be written
341
 
        to the target destination.
342
 
 
343
 
        New file takes the permissions of any existing file with that id,
344
 
        unless mode_id is specified.
345
 
        """
346
 
        name = self._limbo_name(trans_id)
347
 
        f = open(name, 'wb')
348
 
        try:
349
 
            try:
350
 
                unique_add(self._new_contents, trans_id, 'file')
351
 
            except:
352
 
                # Clean up the file, it never got registered so
353
 
                # TreeTransform.finalize() won't clean it up.
354
 
                f.close()
355
 
                os.unlink(name)
356
 
                raise
357
 
 
358
 
            f.writelines(contents)
359
 
        finally:
360
 
            f.close()
361
 
        self._set_mode(trans_id, mode_id, S_ISREG)
362
 
 
363
 
    def _set_mode(self, trans_id, mode_id, typefunc):
364
 
        """Set the mode of new file contents.
365
 
        The mode_id is the existing file to get the mode from (often the same
366
 
        as trans_id).  The operation is only performed if there's a mode match
367
 
        according to typefunc.
368
 
        """
369
 
        if mode_id is None:
370
 
            mode_id = trans_id
371
 
        try:
372
 
            old_path = self._tree_id_paths[mode_id]
373
 
        except KeyError:
374
 
            return
375
 
        try:
376
 
            mode = os.stat(self._tree.abspath(old_path)).st_mode
377
 
        except OSError, e:
378
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
379
 
                # Either old_path doesn't exist, or the parent of the
380
 
                # target is not a directory (but will be one eventually)
381
 
                # Either way, we know it doesn't exist *right now*
382
 
                # See also bug #248448
383
 
                return
384
 
            else:
385
 
                raise
386
 
        if typefunc(mode):
387
 
            os.chmod(self._limbo_name(trans_id), mode)
388
 
 
389
 
    def create_hardlink(self, path, trans_id):
390
 
        """Schedule creation of a hard link"""
391
 
        name = self._limbo_name(trans_id)
392
 
        try:
393
 
            os.link(path, name)
394
 
        except OSError, e:
395
 
            if e.errno != errno.EPERM:
396
 
                raise
397
 
            raise errors.HardLinkNotSupported(path)
398
 
        try:
399
 
            unique_add(self._new_contents, trans_id, 'file')
400
 
        except:
401
 
            # Clean up the file, it never got registered so
402
 
            # TreeTransform.finalize() won't clean it up.
403
 
            os.unlink(name)
404
 
            raise
405
 
 
406
 
    def create_directory(self, trans_id):
407
 
        """Schedule creation of a new directory.
408
 
 
409
 
        See also new_directory.
410
 
        """
411
 
        os.mkdir(self._limbo_name(trans_id))
412
 
        unique_add(self._new_contents, trans_id, 'directory')
413
 
 
414
 
    def create_symlink(self, target, trans_id):
415
 
        """Schedule creation of a new symbolic link.
416
 
 
417
 
        target is a bytestring.
418
 
        See also new_symlink.
419
 
        """
420
 
        if has_symlinks():
421
 
            os.symlink(target, self._limbo_name(trans_id))
422
 
            unique_add(self._new_contents, trans_id, 'symlink')
423
 
        else:
424
 
            try:
425
 
                path = FinalPaths(self).get_path(trans_id)
426
 
            except KeyError:
427
 
                path = None
428
 
            raise UnableCreateSymlink(path=path)
429
 
 
430
 
    def cancel_creation(self, trans_id):
431
 
        """Cancel the creation of new file contents."""
432
 
        del self._new_contents[trans_id]
433
 
        children = self._limbo_children.get(trans_id)
434
 
        # if this is a limbo directory with children, move them before removing
435
 
        # the directory
436
 
        if children is not None:
437
 
            self._rename_in_limbo(children)
438
 
            del self._limbo_children[trans_id]
439
 
            del self._limbo_children_names[trans_id]
440
 
        delete_any(self._limbo_name(trans_id))
441
 
 
442
254
    def delete_contents(self, trans_id):
443
255
        """Schedule the contents of a path entry for deletion"""
444
256
        self.tree_kind(trans_id)
518
330
        new_ids.update(changed_kind)
519
331
        return sorted(FinalPaths(self).get_paths(new_ids))
520
332
 
521
 
    def tree_kind(self, trans_id):
522
 
        """Determine the file kind in the working tree.
523
 
 
524
 
        Raises NoSuchFile if the file does not exist
525
 
        """
526
 
        path = self._tree_id_paths.get(trans_id)
527
 
        if path is None:
528
 
            raise NoSuchFile(None)
529
 
        try:
530
 
            return file_kind(self._tree.abspath(path))
531
 
        except OSError, e:
532
 
            if e.errno != errno.ENOENT:
533
 
                raise
534
 
            else:
535
 
                raise NoSuchFile(path)
536
 
 
537
333
    def final_kind(self, trans_id):
538
334
        """Determine the final file kind, after any changes applied.
539
335
 
667
463
            # ensure that all children are registered with the transaction
668
464
            list(self.iter_tree_children(parent_id))
669
465
 
670
 
    def iter_tree_children(self, parent_id):
671
 
        """Iterate through the entry's tree children, if any"""
672
 
        try:
673
 
            path = self._tree_id_paths[parent_id]
674
 
        except KeyError:
675
 
            return
676
 
        try:
677
 
            children = os.listdir(self._tree.abspath(path))
678
 
        except OSError, e:
679
 
            if not (osutils._is_error_enotdir(e)
680
 
                    or e.errno in (errno.ENOENT, errno.ESRCH)):
681
 
                raise
682
 
            return
683
 
 
684
 
        for child in children:
685
 
            childpath = joinpath(path, child)
686
 
            if self._tree.is_control_filename(childpath):
687
 
                continue
688
 
            yield self.trans_id_tree_path(childpath)
689
 
 
690
466
    def has_named_child(self, by_parent, parent_id, name):
691
467
        try:
692
468
            children = by_parent[parent_id]
867
643
            return True
868
644
        return False
869
645
 
870
 
    def _limbo_name(self, trans_id):
871
 
        """Generate the limbo name of a file"""
872
 
        limbo_name = self._limbo_files.get(trans_id)
873
 
        if limbo_name is not None:
874
 
            return limbo_name
875
 
        parent = self._new_parent.get(trans_id)
876
 
        # if the parent directory is already in limbo (e.g. when building a
877
 
        # tree), choose a limbo name inside the parent, to reduce further
878
 
        # renames.
879
 
        use_direct_path = False
880
 
        if self._new_contents.get(parent) == 'directory':
881
 
            filename = self._new_name.get(trans_id)
882
 
            if filename is not None:
883
 
                if parent not in self._limbo_children:
884
 
                    self._limbo_children[parent] = set()
885
 
                    self._limbo_children_names[parent] = {}
886
 
                    use_direct_path = True
887
 
                # the direct path can only be used if no other file has
888
 
                # already taken this pathname, i.e. if the name is unused, or
889
 
                # if it is already associated with this trans_id.
890
 
                elif self._case_sensitive_target:
891
 
                    if (self._limbo_children_names[parent].get(filename)
892
 
                        in (trans_id, None)):
893
 
                        use_direct_path = True
894
 
                else:
895
 
                    for l_filename, l_trans_id in\
896
 
                        self._limbo_children_names[parent].iteritems():
897
 
                        if l_trans_id == trans_id:
898
 
                            continue
899
 
                        if l_filename.lower() == filename.lower():
900
 
                            break
901
 
                    else:
902
 
                        use_direct_path = True
903
 
 
904
 
        if use_direct_path:
905
 
            limbo_name = pathjoin(self._limbo_files[parent], filename)
906
 
            self._limbo_children[parent].add(trans_id)
907
 
            self._limbo_children_names[parent][filename] = trans_id
908
 
        else:
909
 
            limbo_name = pathjoin(self._limbodir, trans_id)
910
 
            self._needs_rename.add(trans_id)
911
 
        self._limbo_files[trans_id] = limbo_name
912
 
        return limbo_name
913
 
 
914
646
    def _set_executability(self, path, trans_id):
915
647
        """Set the executability of versioned files """
916
648
        if supports_executable():
1176
908
                                      (('attribs',),))
1177
909
        for trans_id, kind in self._new_contents.items():
1178
910
            if kind == 'file':
1179
 
                cur_file = open(self._limbo_name(trans_id), 'rb')
1180
 
                try:
1181
 
                    lines = osutils.chunks_to_lines(cur_file.readlines())
1182
 
                finally:
1183
 
                    cur_file.close()
 
911
                lines = osutils.chunks_to_lines(
 
912
                    self._read_file_chunks(trans_id))
1184
913
                parents = self._get_parents_lines(trans_id)
1185
914
                mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1186
915
                content = ''.join(mpdiff.to_patch())
1187
916
            if kind == 'directory':
1188
917
                content = ''
1189
918
            if kind == 'symlink':
1190
 
                content = os.readlink(self._limbo_name(trans_id))
 
919
                content = self._read_symlink_target(trans_id)
1191
920
            yield serializer.bytes_record(content, ((trans_id, kind),))
1192
921
 
1193
 
 
1194
922
    def deserialize(self, records):
1195
923
        """Deserialize a stored TreeTransform.
1196
924
 
1227
955
                self.create_symlink(content.decode('utf-8'), trans_id)
1228
956
 
1229
957
 
1230
 
class TreeTransform(TreeTransformBase):
 
958
class DiskTreeTransform(TreeTransformBase):
 
959
    """Tree transform storing its contents on disk."""
 
960
 
 
961
    def __init__(self, tree, limbodir, pb=DummyProgress(),
 
962
                 case_sensitive=True):
 
963
        """Constructor.
 
964
        :param tree: The tree that will be transformed, but not necessarily
 
965
            the output tree.
 
966
        :param limbodir: A directory where new files can be stored until
 
967
            they are installed in their proper places
 
968
        :param pb: A ProgressBar indicating how much progress is being made
 
969
        :param case_sensitive: If True, the target of the transform is
 
970
            case sensitive, not just case preserving.
 
971
        """
 
972
        TreeTransformBase.__init__(self, tree, pb, case_sensitive)
 
973
        self._limbodir = limbodir
 
974
        self._deletiondir = None
 
975
        # A mapping of transform ids to their limbo filename
 
976
        self._limbo_files = {}
 
977
        # A mapping of transform ids to a set of the transform ids of children
 
978
        # that their limbo directory has
 
979
        self._limbo_children = {}
 
980
        # Map transform ids to maps of child filename to child transform id
 
981
        self._limbo_children_names = {}
 
982
        # List of transform ids that need to be renamed from limbo into place
 
983
        self._needs_rename = set()
 
984
 
 
985
    def finalize(self):
 
986
        """Release the working tree lock, if held, clean up limbo dir.
 
987
 
 
988
        This is required if apply has not been invoked, but can be invoked
 
989
        even after apply.
 
990
        """
 
991
        if self._tree is None:
 
992
            return
 
993
        try:
 
994
            entries = [(self._limbo_name(t), t, k) for t, k in
 
995
                       self._new_contents.iteritems()]
 
996
            entries.sort(reverse=True)
 
997
            for path, trans_id, kind in entries:
 
998
                if kind == "directory":
 
999
                    os.rmdir(path)
 
1000
                else:
 
1001
                    os.unlink(path)
 
1002
            try:
 
1003
                os.rmdir(self._limbodir)
 
1004
            except OSError:
 
1005
                # We don't especially care *why* the dir is immortal.
 
1006
                raise ImmortalLimbo(self._limbodir)
 
1007
            try:
 
1008
                if self._deletiondir is not None:
 
1009
                    os.rmdir(self._deletiondir)
 
1010
            except OSError:
 
1011
                raise errors.ImmortalPendingDeletion(self._deletiondir)
 
1012
        finally:
 
1013
            TreeTransformBase.finalize(self)
 
1014
 
 
1015
    def _limbo_name(self, trans_id):
 
1016
        """Generate the limbo name of a file"""
 
1017
        limbo_name = self._limbo_files.get(trans_id)
 
1018
        if limbo_name is not None:
 
1019
            return limbo_name
 
1020
        parent = self._new_parent.get(trans_id)
 
1021
        # if the parent directory is already in limbo (e.g. when building a
 
1022
        # tree), choose a limbo name inside the parent, to reduce further
 
1023
        # renames.
 
1024
        use_direct_path = False
 
1025
        if self._new_contents.get(parent) == 'directory':
 
1026
            filename = self._new_name.get(trans_id)
 
1027
            if filename is not None:
 
1028
                if parent not in self._limbo_children:
 
1029
                    self._limbo_children[parent] = set()
 
1030
                    self._limbo_children_names[parent] = {}
 
1031
                    use_direct_path = True
 
1032
                # the direct path can only be used if no other file has
 
1033
                # already taken this pathname, i.e. if the name is unused, or
 
1034
                # if it is already associated with this trans_id.
 
1035
                elif self._case_sensitive_target:
 
1036
                    if (self._limbo_children_names[parent].get(filename)
 
1037
                        in (trans_id, None)):
 
1038
                        use_direct_path = True
 
1039
                else:
 
1040
                    for l_filename, l_trans_id in\
 
1041
                        self._limbo_children_names[parent].iteritems():
 
1042
                        if l_trans_id == trans_id:
 
1043
                            continue
 
1044
                        if l_filename.lower() == filename.lower():
 
1045
                            break
 
1046
                    else:
 
1047
                        use_direct_path = True
 
1048
 
 
1049
        if use_direct_path:
 
1050
            limbo_name = pathjoin(self._limbo_files[parent], filename)
 
1051
            self._limbo_children[parent].add(trans_id)
 
1052
            self._limbo_children_names[parent][filename] = trans_id
 
1053
        else:
 
1054
            limbo_name = pathjoin(self._limbodir, trans_id)
 
1055
            self._needs_rename.add(trans_id)
 
1056
        self._limbo_files[trans_id] = limbo_name
 
1057
        return limbo_name
 
1058
 
 
1059
    def adjust_path(self, name, parent, trans_id):
 
1060
        previous_parent = self._new_parent.get(trans_id)
 
1061
        previous_name = self._new_name.get(trans_id)
 
1062
        TreeTransformBase.adjust_path(self, name, parent, trans_id)
 
1063
        if (trans_id in self._limbo_files and
 
1064
            trans_id not in self._needs_rename):
 
1065
            self._rename_in_limbo([trans_id])
 
1066
            self._limbo_children[previous_parent].remove(trans_id)
 
1067
            del self._limbo_children_names[previous_parent][previous_name]
 
1068
 
 
1069
    def _rename_in_limbo(self, trans_ids):
 
1070
        """Fix limbo names so that the right final path is produced.
 
1071
 
 
1072
        This means we outsmarted ourselves-- we tried to avoid renaming
 
1073
        these files later by creating them with their final names in their
 
1074
        final parents.  But now the previous name or parent is no longer
 
1075
        suitable, so we have to rename them.
 
1076
 
 
1077
        Even for trans_ids that have no new contents, we must remove their
 
1078
        entries from _limbo_files, because they are now stale.
 
1079
        """
 
1080
        for trans_id in trans_ids:
 
1081
            old_path = self._limbo_files.pop(trans_id)
 
1082
            if trans_id not in self._new_contents:
 
1083
                continue
 
1084
            new_path = self._limbo_name(trans_id)
 
1085
            os.rename(old_path, new_path)
 
1086
 
 
1087
    def create_file(self, contents, trans_id, mode_id=None):
 
1088
        """Schedule creation of a new file.
 
1089
 
 
1090
        See also new_file.
 
1091
 
 
1092
        Contents is an iterator of strings, all of which will be written
 
1093
        to the target destination.
 
1094
 
 
1095
        New file takes the permissions of any existing file with that id,
 
1096
        unless mode_id is specified.
 
1097
        """
 
1098
        name = self._limbo_name(trans_id)
 
1099
        f = open(name, 'wb')
 
1100
        try:
 
1101
            try:
 
1102
                unique_add(self._new_contents, trans_id, 'file')
 
1103
            except:
 
1104
                # Clean up the file, it never got registered so
 
1105
                # TreeTransform.finalize() won't clean it up.
 
1106
                f.close()
 
1107
                os.unlink(name)
 
1108
                raise
 
1109
 
 
1110
            f.writelines(contents)
 
1111
        finally:
 
1112
            f.close()
 
1113
        self._set_mode(trans_id, mode_id, S_ISREG)
 
1114
 
 
1115
    def _read_file_chunks(self, trans_id):
 
1116
        cur_file = open(self._limbo_name(trans_id), 'rb')
 
1117
        try:
 
1118
            return cur_file.readlines()
 
1119
        finally:
 
1120
            cur_file.close()
 
1121
 
 
1122
    def _read_symlink_target(self, trans_id):
 
1123
        return os.readlink(self._limbo_name(trans_id))
 
1124
 
 
1125
    def create_hardlink(self, path, trans_id):
 
1126
        """Schedule creation of a hard link"""
 
1127
        name = self._limbo_name(trans_id)
 
1128
        try:
 
1129
            os.link(path, name)
 
1130
        except OSError, e:
 
1131
            if e.errno != errno.EPERM:
 
1132
                raise
 
1133
            raise errors.HardLinkNotSupported(path)
 
1134
        try:
 
1135
            unique_add(self._new_contents, trans_id, 'file')
 
1136
        except:
 
1137
            # Clean up the file, it never got registered so
 
1138
            # TreeTransform.finalize() won't clean it up.
 
1139
            os.unlink(name)
 
1140
            raise
 
1141
 
 
1142
    def create_directory(self, trans_id):
 
1143
        """Schedule creation of a new directory.
 
1144
 
 
1145
        See also new_directory.
 
1146
        """
 
1147
        os.mkdir(self._limbo_name(trans_id))
 
1148
        unique_add(self._new_contents, trans_id, 'directory')
 
1149
 
 
1150
    def create_symlink(self, target, trans_id):
 
1151
        """Schedule creation of a new symbolic link.
 
1152
 
 
1153
        target is a bytestring.
 
1154
        See also new_symlink.
 
1155
        """
 
1156
        if has_symlinks():
 
1157
            os.symlink(target, self._limbo_name(trans_id))
 
1158
            unique_add(self._new_contents, trans_id, 'symlink')
 
1159
        else:
 
1160
            try:
 
1161
                path = FinalPaths(self).get_path(trans_id)
 
1162
            except KeyError:
 
1163
                path = None
 
1164
            raise UnableCreateSymlink(path=path)
 
1165
 
 
1166
    def cancel_creation(self, trans_id):
 
1167
        """Cancel the creation of new file contents."""
 
1168
        del self._new_contents[trans_id]
 
1169
        children = self._limbo_children.get(trans_id)
 
1170
        # if this is a limbo directory with children, move them before removing
 
1171
        # the directory
 
1172
        if children is not None:
 
1173
            self._rename_in_limbo(children)
 
1174
            del self._limbo_children[trans_id]
 
1175
            del self._limbo_children_names[trans_id]
 
1176
        delete_any(self._limbo_name(trans_id))
 
1177
 
 
1178
 
 
1179
class TreeTransform(DiskTreeTransform):
1231
1180
    """Represent a tree transformation.
1232
1181
 
1233
1182
    This object is designed to support incremental generation of the transform,
1319
1268
            tree.unlock()
1320
1269
            raise
1321
1270
 
1322
 
        TreeTransformBase.__init__(self, tree, limbodir, pb,
 
1271
        # Cache of realpath results, to speed up canonical_path
 
1272
        self._realpaths = {}
 
1273
        # Cache of relpath results, to speed up canonical_path
 
1274
        self._relpaths = {}
 
1275
        DiskTreeTransform.__init__(self, tree, limbodir, pb,
1323
1276
                                   tree.case_sensitive)
1324
1277
        self._deletiondir = deletiondir
1325
1278
 
 
1279
    def canonical_path(self, path):
 
1280
        """Get the canonical tree-relative path"""
 
1281
        # don't follow final symlinks
 
1282
        abs = self._tree.abspath(path)
 
1283
        if abs in self._relpaths:
 
1284
            return self._relpaths[abs]
 
1285
        dirname, basename = os.path.split(abs)
 
1286
        if dirname not in self._realpaths:
 
1287
            self._realpaths[dirname] = os.path.realpath(dirname)
 
1288
        dirname = self._realpaths[dirname]
 
1289
        abs = pathjoin(dirname, basename)
 
1290
        if dirname in self._relpaths:
 
1291
            relpath = pathjoin(self._relpaths[dirname], basename)
 
1292
            relpath = relpath.rstrip('/\\')
 
1293
        else:
 
1294
            relpath = self._tree.relpath(abs)
 
1295
        self._relpaths[abs] = relpath
 
1296
        return relpath
 
1297
 
 
1298
    def tree_kind(self, trans_id):
 
1299
        """Determine the file kind in the working tree.
 
1300
 
 
1301
        Raises NoSuchFile if the file does not exist
 
1302
        """
 
1303
        path = self._tree_id_paths.get(trans_id)
 
1304
        if path is None:
 
1305
            raise NoSuchFile(None)
 
1306
        try:
 
1307
            return file_kind(self._tree.abspath(path))
 
1308
        except OSError, e:
 
1309
            if e.errno != errno.ENOENT:
 
1310
                raise
 
1311
            else:
 
1312
                raise NoSuchFile(path)
 
1313
 
 
1314
    def _set_mode(self, trans_id, mode_id, typefunc):
 
1315
        """Set the mode of new file contents.
 
1316
        The mode_id is the existing file to get the mode from (often the same
 
1317
        as trans_id).  The operation is only performed if there's a mode match
 
1318
        according to typefunc.
 
1319
        """
 
1320
        if mode_id is None:
 
1321
            mode_id = trans_id
 
1322
        try:
 
1323
            old_path = self._tree_id_paths[mode_id]
 
1324
        except KeyError:
 
1325
            return
 
1326
        try:
 
1327
            mode = os.stat(self._tree.abspath(old_path)).st_mode
 
1328
        except OSError, e:
 
1329
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
 
1330
                # Either old_path doesn't exist, or the parent of the
 
1331
                # target is not a directory (but will be one eventually)
 
1332
                # Either way, we know it doesn't exist *right now*
 
1333
                # See also bug #248448
 
1334
                return
 
1335
            else:
 
1336
                raise
 
1337
        if typefunc(mode):
 
1338
            os.chmod(self._limbo_name(trans_id), mode)
 
1339
 
 
1340
    def iter_tree_children(self, parent_id):
 
1341
        """Iterate through the entry's tree children, if any"""
 
1342
        try:
 
1343
            path = self._tree_id_paths[parent_id]
 
1344
        except KeyError:
 
1345
            return
 
1346
        try:
 
1347
            children = os.listdir(self._tree.abspath(path))
 
1348
        except OSError, e:
 
1349
            if not (osutils._is_error_enotdir(e)
 
1350
                    or e.errno in (errno.ENOENT, errno.ESRCH)):
 
1351
                raise
 
1352
            return
 
1353
 
 
1354
        for child in children:
 
1355
            childpath = joinpath(path, child)
 
1356
            if self._tree.is_control_filename(childpath):
 
1357
                continue
 
1358
            yield self.trans_id_tree_path(childpath)
 
1359
 
 
1360
 
1326
1361
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1327
1362
        """Apply all changes to the inventory and filesystem.
1328
1363
 
1505
1540
        return modified_paths
1506
1541
 
1507
1542
 
1508
 
class TransformPreview(TreeTransformBase):
 
1543
class TransformPreview(DiskTreeTransform):
1509
1544
    """A TreeTransform for generating preview trees.
1510
1545
 
1511
1546
    Unlike TreeTransform, this version works when the input tree is a
1516
1551
    def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1517
1552
        tree.lock_read()
1518
1553
        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1519
 
        TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
 
1554
        DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1520
1555
 
1521
1556
    def canonical_path(self, path):
1522
1557
        return path