~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Ian Clatworthy
  • Date: 2010-02-19 03:02:07 UTC
  • mto: (4797.23.1 integration-2.1)
  • mto: This revision was merged to the branch mainline in revision 5055.
  • Revision ID: ian.clatworthy@canonical.com-20100219030207-zpbzx021zavx4sqt
What's New in 2.1 - a summary of changes since 2.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007, 2008, 2009 Canonical Ltd
 
1
# Copyright (C) 2006-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
17
17
import os
18
18
import errno
19
19
from stat import S_ISREG, S_IEXEC
 
20
import time
20
21
 
21
22
from bzrlib.lazy_import import lazy_import
22
23
lazy_import(globals(), """
84
85
 
85
86
        :param tree: The tree that will be transformed, but not necessarily
86
87
            the output tree.
87
 
        :param pb: A ProgressBar indicating how much progress is being made
 
88
        :param pb: A ProgressTask indicating how much progress is being made
88
89
        :param case_sensitive: If True, the target of the transform is
89
90
            case sensitive, not just case preserving.
90
91
        """
161
162
 
162
163
    def adjust_path(self, name, parent, trans_id):
163
164
        """Change the path that is assigned to a transaction id."""
 
165
        if parent is None:
 
166
            raise ValueError("Parent trans-id may not be None")
164
167
        if trans_id == self._new_root:
165
168
            raise CantMoveRoot
166
169
        self._new_name[trans_id] = name
167
170
        self._new_parent[trans_id] = parent
168
 
        if parent == ROOT_PARENT:
169
 
            if self._new_root is not None:
170
 
                raise ValueError("Cannot have multiple roots.")
171
 
            self._new_root = trans_id
172
171
 
173
172
    def adjust_root_path(self, name, parent):
174
173
        """Emulate moving the root by moving all children, instead.
202
201
        self.version_file(old_root_file_id, old_root)
203
202
        self.unversion_file(self._new_root)
204
203
 
 
204
    def fixup_new_roots(self):
 
205
        """Reinterpret requests to change the root directory
 
206
 
 
207
        Instead of creating a root directory, or moving an existing directory,
 
208
        all the attributes and children of the new root are applied to the
 
209
        existing root directory.
 
210
 
 
211
        This means that the old root trans-id becomes obsolete, so it is
 
212
        recommended only to invoke this after the root trans-id has become
 
213
        irrelevant.
 
214
        """
 
215
        new_roots = [k for k, v in self._new_parent.iteritems() if v is
 
216
                     ROOT_PARENT]
 
217
        if len(new_roots) < 1:
 
218
            return
 
219
        if len(new_roots) != 1:
 
220
            raise ValueError('A tree cannot have two roots!')
 
221
        if self._new_root is None:
 
222
            self._new_root = new_roots[0]
 
223
            return
 
224
        old_new_root = new_roots[0]
 
225
        # TODO: What to do if a old_new_root is present, but self._new_root is
 
226
        #       not listed as being removed? This code explicitly unversions
 
227
        #       the old root and versions it with the new file_id. Though that
 
228
        #       seems like an incomplete delta
 
229
 
 
230
        # unversion the new root's directory.
 
231
        file_id = self.final_file_id(old_new_root)
 
232
        if old_new_root in self._new_id:
 
233
            self.cancel_versioning(old_new_root)
 
234
        else:
 
235
            self.unversion_file(old_new_root)
 
236
        # if, at this stage, root still has an old file_id, zap it so we can
 
237
        # stick a new one in.
 
238
        if (self.tree_file_id(self._new_root) is not None and
 
239
            self._new_root not in self._removed_id):
 
240
            self.unversion_file(self._new_root)
 
241
        self.version_file(file_id, self._new_root)
 
242
 
 
243
        # Now move children of new root into old root directory.
 
244
        # Ensure all children are registered with the transaction, but don't
 
245
        # use directly-- some tree children have new parents
 
246
        list(self.iter_tree_children(old_new_root))
 
247
        # Move all children of new root into old root directory.
 
248
        for child in self.by_parent().get(old_new_root, []):
 
249
            self.adjust_path(self.final_name(child), self._new_root, child)
 
250
 
 
251
        # Ensure old_new_root has no directory.
 
252
        if old_new_root in self._new_contents:
 
253
            self.cancel_creation(old_new_root)
 
254
        else:
 
255
            self.delete_contents(old_new_root)
 
256
 
 
257
        # prevent deletion of root directory.
 
258
        if self._new_root in self._removed_contents:
 
259
            self.cancel_deletion(self._new_root)
 
260
 
 
261
        # destroy path info for old_new_root.
 
262
        del self._new_parent[old_new_root]
 
263
        del self._new_name[old_new_root]
 
264
 
205
265
    def trans_id_tree_file_id(self, inventory_id):
206
266
        """Determine the transaction id of a working tree file.
207
267
 
253
313
 
254
314
    def delete_contents(self, trans_id):
255
315
        """Schedule the contents of a path entry for deletion"""
 
316
        # Ensure that the object exists in the WorkingTree, this will raise an
 
317
        # exception if there is a problem
256
318
        self.tree_kind(trans_id)
257
319
        self._removed_contents.add(trans_id)
258
320
 
859
921
    def get_preview_tree(self):
860
922
        """Return a tree representing the result of the transform.
861
923
 
862
 
        This tree only supports the subset of Tree functionality required
863
 
        by show_diff_trees.  It must only be compared to tt._tree.
 
924
        The tree is a snapshot, and altering the TreeTransform will invalidate
 
925
        it.
864
926
        """
865
927
        return _PreviewTree(self)
866
928
 
1023
1085
        self._limbo_children_names = {}
1024
1086
        # List of transform ids that need to be renamed from limbo into place
1025
1087
        self._needs_rename = set()
 
1088
        self._creation_mtime = None
1026
1089
 
1027
1090
    def finalize(self):
1028
1091
        """Release the working tree lock, if held, clean up limbo dir.
1054
1117
    def _limbo_name(self, trans_id):
1055
1118
        """Generate the limbo name of a file"""
1056
1119
        limbo_name = self._limbo_files.get(trans_id)
1057
 
        if limbo_name is not None:
1058
 
            return limbo_name
1059
 
        parent = self._new_parent.get(trans_id)
1060
 
        # if the parent directory is already in limbo (e.g. when building a
1061
 
        # tree), choose a limbo name inside the parent, to reduce further
1062
 
        # renames.
1063
 
        use_direct_path = False
1064
 
        if self._new_contents.get(parent) == 'directory':
1065
 
            filename = self._new_name.get(trans_id)
1066
 
            if filename is not None:
1067
 
                if parent not in self._limbo_children:
1068
 
                    self._limbo_children[parent] = set()
1069
 
                    self._limbo_children_names[parent] = {}
1070
 
                    use_direct_path = True
1071
 
                # the direct path can only be used if no other file has
1072
 
                # already taken this pathname, i.e. if the name is unused, or
1073
 
                # if it is already associated with this trans_id.
1074
 
                elif self._case_sensitive_target:
1075
 
                    if (self._limbo_children_names[parent].get(filename)
1076
 
                        in (trans_id, None)):
1077
 
                        use_direct_path = True
1078
 
                else:
1079
 
                    for l_filename, l_trans_id in\
1080
 
                        self._limbo_children_names[parent].iteritems():
1081
 
                        if l_trans_id == trans_id:
1082
 
                            continue
1083
 
                        if l_filename.lower() == filename.lower():
1084
 
                            break
1085
 
                    else:
1086
 
                        use_direct_path = True
1087
 
 
1088
 
        if use_direct_path:
1089
 
            limbo_name = pathjoin(self._limbo_files[parent], filename)
1090
 
            self._limbo_children[parent].add(trans_id)
1091
 
            self._limbo_children_names[parent][filename] = trans_id
1092
 
        else:
1093
 
            limbo_name = pathjoin(self._limbodir, trans_id)
1094
 
            self._needs_rename.add(trans_id)
1095
 
        self._limbo_files[trans_id] = limbo_name
 
1120
        if limbo_name is None:
 
1121
            limbo_name = self._generate_limbo_path(trans_id)
 
1122
            self._limbo_files[trans_id] = limbo_name
1096
1123
        return limbo_name
1097
1124
 
 
1125
    def _generate_limbo_path(self, trans_id):
 
1126
        """Generate a limbo path using the trans_id as the relative path.
 
1127
 
 
1128
        This is suitable as a fallback, and when the transform should not be
 
1129
        sensitive to the path encoding of the limbo directory.
 
1130
        """
 
1131
        self._needs_rename.add(trans_id)
 
1132
        return pathjoin(self._limbodir, trans_id)
 
1133
 
1098
1134
    def adjust_path(self, name, parent, trans_id):
1099
1135
        previous_parent = self._new_parent.get(trans_id)
1100
1136
        previous_name = self._new_name.get(trans_id)
1102
1138
        if (trans_id in self._limbo_files and
1103
1139
            trans_id not in self._needs_rename):
1104
1140
            self._rename_in_limbo([trans_id])
1105
 
            self._limbo_children[previous_parent].remove(trans_id)
1106
 
            del self._limbo_children_names[previous_parent][previous_name]
 
1141
            if previous_parent != parent:
 
1142
                self._limbo_children[previous_parent].remove(trans_id)
 
1143
            if previous_parent != parent or previous_name != name:
 
1144
                del self._limbo_children_names[previous_parent][previous_name]
1107
1145
 
1108
1146
    def _rename_in_limbo(self, trans_ids):
1109
1147
        """Fix limbo names so that the right final path is produced.
1122
1160
                continue
1123
1161
            new_path = self._limbo_name(trans_id)
1124
1162
            os.rename(old_path, new_path)
 
1163
            for descendant in self._limbo_descendants(trans_id):
 
1164
                desc_path = self._limbo_files[descendant]
 
1165
                desc_path = new_path + desc_path[len(old_path):]
 
1166
                self._limbo_files[descendant] = desc_path
 
1167
 
 
1168
    def _limbo_descendants(self, trans_id):
 
1169
        """Return the set of trans_ids whose limbo paths descend from this."""
 
1170
        descendants = set(self._limbo_children.get(trans_id, []))
 
1171
        for descendant in list(descendants):
 
1172
            descendants.update(self._limbo_descendants(descendant))
 
1173
        return descendants
1125
1174
 
1126
1175
    def create_file(self, contents, trans_id, mode_id=None):
1127
1176
        """Schedule creation of a new file.
1149
1198
            f.writelines(contents)
1150
1199
        finally:
1151
1200
            f.close()
 
1201
        self._set_mtime(name)
1152
1202
        self._set_mode(trans_id, mode_id, S_ISREG)
1153
1203
 
1154
1204
    def _read_file_chunks(self, trans_id):
1161
1211
    def _read_symlink_target(self, trans_id):
1162
1212
        return os.readlink(self._limbo_name(trans_id))
1163
1213
 
 
1214
    def _set_mtime(self, path):
 
1215
        """All files that are created get the same mtime.
 
1216
 
 
1217
        This time is set by the first object to be created.
 
1218
        """
 
1219
        if self._creation_mtime is None:
 
1220
            self._creation_mtime = time.time()
 
1221
        os.utime(path, (self._creation_mtime, self._creation_mtime))
 
1222
 
1164
1223
    def create_hardlink(self, path, trans_id):
1165
1224
        """Schedule creation of a hard link"""
1166
1225
        name = self._limbo_name(trans_id)
1396
1455
                continue
1397
1456
            yield self.trans_id_tree_path(childpath)
1398
1457
 
 
1458
    def _generate_limbo_path(self, trans_id):
 
1459
        """Generate a limbo path using the final path if possible.
 
1460
 
 
1461
        This optimizes the performance of applying the tree transform by
 
1462
        avoiding renames.  These renames can be avoided only when the parent
 
1463
        directory is already scheduled for creation.
 
1464
 
 
1465
        If the final path cannot be used, falls back to using the trans_id as
 
1466
        the relpath.
 
1467
        """
 
1468
        parent = self._new_parent.get(trans_id)
 
1469
        # if the parent directory is already in limbo (e.g. when building a
 
1470
        # tree), choose a limbo name inside the parent, to reduce further
 
1471
        # renames.
 
1472
        use_direct_path = False
 
1473
        if self._new_contents.get(parent) == 'directory':
 
1474
            filename = self._new_name.get(trans_id)
 
1475
            if filename is not None:
 
1476
                if parent not in self._limbo_children:
 
1477
                    self._limbo_children[parent] = set()
 
1478
                    self._limbo_children_names[parent] = {}
 
1479
                    use_direct_path = True
 
1480
                # the direct path can only be used if no other file has
 
1481
                # already taken this pathname, i.e. if the name is unused, or
 
1482
                # if it is already associated with this trans_id.
 
1483
                elif self._case_sensitive_target:
 
1484
                    if (self._limbo_children_names[parent].get(filename)
 
1485
                        in (trans_id, None)):
 
1486
                        use_direct_path = True
 
1487
                else:
 
1488
                    for l_filename, l_trans_id in\
 
1489
                        self._limbo_children_names[parent].iteritems():
 
1490
                        if l_trans_id == trans_id:
 
1491
                            continue
 
1492
                        if l_filename.lower() == filename.lower():
 
1493
                            break
 
1494
                    else:
 
1495
                        use_direct_path = True
 
1496
 
 
1497
        if not use_direct_path:
 
1498
            return DiskTreeTransform._generate_limbo_path(self, trans_id)
 
1499
 
 
1500
        limbo_name = pathjoin(self._limbo_files[parent], filename)
 
1501
        self._limbo_children[parent].add(trans_id)
 
1502
        self._limbo_children_names[parent][filename] = trans_id
 
1503
        return limbo_name
 
1504
 
 
1505
 
1399
1506
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1400
1507
        """Apply all changes to the inventory and filesystem.
1401
1508
 
1521
1628
                child_pb.update('removing file', num, len(tree_paths))
1522
1629
                full_path = self._tree.abspath(path)
1523
1630
                if trans_id in self._removed_contents:
1524
 
                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
1525
 
                                     trans_id))
1526
 
                elif trans_id in self._new_name or trans_id in \
1527
 
                    self._new_parent:
 
1631
                    delete_path = os.path.join(self._deletiondir, trans_id)
 
1632
                    mover.pre_delete(full_path, delete_path)
 
1633
                elif (trans_id in self._new_name
 
1634
                      or trans_id in self._new_parent):
1528
1635
                    try:
1529
1636
                        mover.rename(full_path, self._limbo_name(trans_id))
1530
1637
                    except OSError, e:
1635
1742
        self._all_children_cache = {}
1636
1743
        self._path2trans_id_cache = {}
1637
1744
        self._final_name_cache = {}
1638
 
 
1639
 
    def _changes(self, file_id):
1640
 
        for changes in self._transform.iter_changes():
1641
 
            if changes[0] == file_id:
1642
 
                return changes
 
1745
        self._iter_changes_cache = dict((c[0], c) for c in
 
1746
                                        self._transform.iter_changes())
1643
1747
 
1644
1748
    def _content_change(self, file_id):
1645
1749
        """Return True if the content of this file changed"""
1646
 
        changes = self._changes(file_id)
 
1750
        changes = self._iter_changes_cache.get(file_id)
1647
1751
        # changes[2] is true if the file content changed.  See
1648
1752
        # InterTree.iter_changes.
1649
1753
        return (changes is not None and changes[2])
1790
1894
            if self._transform.final_file_id(trans_id) is None:
1791
1895
                yield self._final_paths._determine_path(trans_id)
1792
1896
 
1793
 
    def _make_inv_entries(self, ordered_entries, specific_file_ids=None):
 
1897
    def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
 
1898
        yield_parents=False):
1794
1899
        for trans_id, parent_file_id in ordered_entries:
1795
1900
            file_id = self._transform.final_file_id(trans_id)
1796
1901
            if file_id is None:
1822
1927
                ordered_ids.append((trans_id, parent_file_id))
1823
1928
        return ordered_ids
1824
1929
 
1825
 
    def iter_entries_by_dir(self, specific_file_ids=None):
 
1930
    def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
1826
1931
        # This may not be a maximally efficient implementation, but it is
1827
1932
        # reasonably straightforward.  An implementation that grafts the
1828
1933
        # TreeTransform changes onto the tree's iter_entries_by_dir results
1830
1935
        # position.
1831
1936
        ordered_ids = self._list_files_by_dir()
1832
1937
        for entry, trans_id in self._make_inv_entries(ordered_ids,
1833
 
                                                      specific_file_ids):
 
1938
            specific_file_ids, yield_parents=yield_parents):
1834
1939
            yield unicode(self._final_paths.get_path(trans_id)), entry
1835
1940
 
1836
1941
    def _iter_entries_for_dir(self, dir_path):
1883
1988
    def get_file_mtime(self, file_id, path=None):
1884
1989
        """See Tree.get_file_mtime"""
1885
1990
        if not self._content_change(file_id):
1886
 
            return self._transform._tree.get_file_mtime(file_id, path)
 
1991
            return self._transform._tree.get_file_mtime(file_id)
1887
1992
        return self._stat_limbo_file(file_id).st_mtime
1888
1993
 
1889
1994
    def _file_size(self, entry, stat_value):
1943
2048
                statval = os.lstat(limbo_name)
1944
2049
                size = statval.st_size
1945
2050
                if not supports_executable():
1946
 
                    executable = None
 
2051
                    executable = False
1947
2052
                else:
1948
2053
                    executable = statval.st_mode & S_IEXEC
1949
2054
            else:
1951
2056
                executable = None
1952
2057
            if kind == 'symlink':
1953
2058
                link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
1954
 
        if supports_executable():
1955
 
            executable = tt._new_executability.get(trans_id, executable)
 
2059
        executable = tt._new_executability.get(trans_id, executable)
1956
2060
        return kind, size, executable, link_or_sha1
1957
2061
 
1958
2062
    def iter_changes(self, from_tree, include_unchanged=False,
1989
2093
 
1990
2094
    def annotate_iter(self, file_id,
1991
2095
                      default_revision=_mod_revision.CURRENT_REVISION):
1992
 
        changes = self._changes(file_id)
 
2096
        changes = self._iter_changes_cache.get(file_id)
1993
2097
        if changes is None:
1994
2098
            get_old = True
1995
2099
        else:
2271
2375
        new_desired_files = desired_files
2272
2376
    else:
2273
2377
        iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2274
 
        unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
2275
 
                         in iter if not (c or e[0] != e[1]))
 
2378
        unchanged = [(f, p[1]) for (f, p, c, v, d, n, k, e)
 
2379
                     in iter if not (c or e[0] != e[1])]
 
2380
        if accelerator_tree.supports_content_filtering():
 
2381
            unchanged = [(f, p) for (f, p) in unchanged
 
2382
                         if not accelerator_tree.iter_search_rules([p]).next()]
 
2383
        unchanged = dict(unchanged)
2276
2384
        new_desired_files = []
2277
2385
        count = 0
2278
2386
        for file_id, (trans_id, tree_path) in desired_files:
2401
2509
        tt.create_directory(trans_id)
2402
2510
 
2403
2511
 
2404
 
def create_from_tree(tt, trans_id, tree, file_id, bytes=None):
2405
 
    """Create new file contents according to tree contents."""
 
2512
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
 
2513
    filter_tree_path=None):
 
2514
    """Create new file contents according to tree contents.
 
2515
    
 
2516
    :param filter_tree_path: the tree path to use to lookup
 
2517
      content filters to apply to the bytes output in the working tree.
 
2518
      This only applies if the working tree supports content filtering.
 
2519
    """
2406
2520
    kind = tree.kind(file_id)
2407
2521
    if kind == 'directory':
2408
2522
        tt.create_directory(trans_id)
2413
2527
                bytes = tree_file.readlines()
2414
2528
            finally:
2415
2529
                tree_file.close()
 
2530
        wt = tt._tree
 
2531
        if wt.supports_content_filtering() and filter_tree_path is not None:
 
2532
            filters = wt._content_filter_stack(filter_tree_path)
 
2533
            bytes = filtered_output_bytes(bytes, filters,
 
2534
                ContentFilterContext(filter_tree_path, tree))
2416
2535
        tt.create_file(bytes, trans_id)
2417
2536
    elif kind == "symlink":
2418
2537
        tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2606
2725
                    parent_trans = ROOT_PARENT
2607
2726
                else:
2608
2727
                    parent_trans = tt.trans_id_file_id(parent[1])
2609
 
                tt.adjust_path(name[1], parent_trans, trans_id)
 
2728
                if parent[0] is None and versioned[0]:
 
2729
                    tt.adjust_root_path(name[1], parent_trans)
 
2730
                else:
 
2731
                    tt.adjust_path(name[1], parent_trans, trans_id)
2610
2732
            if executable[0] != executable[1] and kind[1] == "file":
2611
2733
                tt.set_executability(executable[1], trans_id)
2612
 
        for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2613
 
            deferred_files):
2614
 
            tt.create_file(bytes, trans_id, mode_id)
 
2734
        if working_tree.supports_content_filtering():
 
2735
            for index, ((trans_id, mode_id), bytes) in enumerate(
 
2736
                target_tree.iter_files_bytes(deferred_files)):
 
2737
                file_id = deferred_files[index][0]
 
2738
                # We're reverting a tree to the target tree so using the
 
2739
                # target tree to find the file path seems the best choice
 
2740
                # here IMO - Ian C 27/Oct/2009
 
2741
                filter_tree_path = target_tree.id2path(file_id)
 
2742
                filters = working_tree._content_filter_stack(filter_tree_path)
 
2743
                bytes = filtered_output_bytes(bytes, filters,
 
2744
                    ContentFilterContext(filter_tree_path, working_tree))
 
2745
                tt.create_file(bytes, trans_id, mode_id)
 
2746
        else:
 
2747
            for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
 
2748
                deferred_files):
 
2749
                tt.create_file(bytes, trans_id, mode_id)
 
2750
        tt.fixup_new_roots()
2615
2751
    finally:
2616
2752
        if basis_tree is not None:
2617
2753
            basis_tree.unlock()