~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

  • Committer: Andrew Bennetts
  • Date: 2010-02-12 04:33:05 UTC
  • mfrom: (5031 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5032.
  • Revision ID: andrew.bennetts@canonical.com-20100212043305-ujdbsdoviql2t7i3
Merge lp:bzr

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(), """
30
31
    multiparent,
31
32
    osutils,
32
33
    revision as _mod_revision,
 
34
    ui,
33
35
    )
34
36
""")
35
37
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
48
50
    splitpath,
49
51
    supports_executable,
50
52
)
51
 
from bzrlib.progress import DummyProgress, ProgressPhase
 
53
from bzrlib.progress import ProgressPhase
52
54
from bzrlib.symbol_versioning import (
53
55
        deprecated_function,
54
56
        deprecated_in,
78
80
class TreeTransformBase(object):
79
81
    """The base class for TreeTransform and its kin."""
80
82
 
81
 
    def __init__(self, tree, pb=DummyProgress(),
 
83
    def __init__(self, tree, pb=None,
82
84
                 case_sensitive=True):
83
85
        """Constructor.
84
86
 
85
87
        :param tree: The tree that will be transformed, but not necessarily
86
88
            the output tree.
87
 
        :param pb: A ProgressTask indicating how much progress is being made
 
89
        :param pb: ignored
88
90
        :param case_sensitive: If True, the target of the transform is
89
91
            case sensitive, not just case preserving.
90
92
        """
161
163
 
162
164
    def adjust_path(self, name, parent, trans_id):
163
165
        """Change the path that is assigned to a transaction id."""
 
166
        if parent is None:
 
167
            raise ValueError("Parent trans-id may not be None")
164
168
        if trans_id == self._new_root:
165
169
            raise CantMoveRoot
166
170
        self._new_name[trans_id] = name
167
171
        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
172
 
173
173
    def adjust_root_path(self, name, parent):
174
174
        """Emulate moving the root by moving all children, instead.
202
202
        self.version_file(old_root_file_id, old_root)
203
203
        self.unversion_file(self._new_root)
204
204
 
 
205
    def fixup_new_roots(self):
 
206
        """Reinterpret requests to change the root directory
 
207
 
 
208
        Instead of creating a root directory, or moving an existing directory,
 
209
        all the attributes and children of the new root are applied to the
 
210
        existing root directory.
 
211
 
 
212
        This means that the old root trans-id becomes obsolete, so it is
 
213
        recommended only to invoke this after the root trans-id has become
 
214
        irrelevant.
 
215
        """
 
216
        new_roots = [k for k, v in self._new_parent.iteritems() if v is
 
217
                     ROOT_PARENT]
 
218
        if len(new_roots) < 1:
 
219
            return
 
220
        if len(new_roots) != 1:
 
221
            raise ValueError('A tree cannot have two roots!')
 
222
        if self._new_root is None:
 
223
            self._new_root = new_roots[0]
 
224
            return
 
225
        old_new_root = new_roots[0]
 
226
        # TODO: What to do if a old_new_root is present, but self._new_root is
 
227
        #       not listed as being removed? This code explicitly unversions
 
228
        #       the old root and versions it with the new file_id. Though that
 
229
        #       seems like an incomplete delta
 
230
 
 
231
        # unversion the new root's directory.
 
232
        file_id = self.final_file_id(old_new_root)
 
233
        if old_new_root in self._new_id:
 
234
            self.cancel_versioning(old_new_root)
 
235
        else:
 
236
            self.unversion_file(old_new_root)
 
237
        # if, at this stage, root still has an old file_id, zap it so we can
 
238
        # stick a new one in.
 
239
        if (self.tree_file_id(self._new_root) is not None and
 
240
            self._new_root not in self._removed_id):
 
241
            self.unversion_file(self._new_root)
 
242
        self.version_file(file_id, self._new_root)
 
243
 
 
244
        # Now move children of new root into old root directory.
 
245
        # Ensure all children are registered with the transaction, but don't
 
246
        # use directly-- some tree children have new parents
 
247
        list(self.iter_tree_children(old_new_root))
 
248
        # Move all children of new root into old root directory.
 
249
        for child in self.by_parent().get(old_new_root, []):
 
250
            self.adjust_path(self.final_name(child), self._new_root, child)
 
251
 
 
252
        # Ensure old_new_root has no directory.
 
253
        if old_new_root in self._new_contents:
 
254
            self.cancel_creation(old_new_root)
 
255
        else:
 
256
            self.delete_contents(old_new_root)
 
257
 
 
258
        # prevent deletion of root directory.
 
259
        if self._new_root in self._removed_contents:
 
260
            self.cancel_deletion(self._new_root)
 
261
 
 
262
        # destroy path info for old_new_root.
 
263
        del self._new_parent[old_new_root]
 
264
        del self._new_name[old_new_root]
 
265
 
205
266
    def trans_id_tree_file_id(self, inventory_id):
206
267
        """Determine the transaction id of a working tree file.
207
268
 
253
314
 
254
315
    def delete_contents(self, trans_id):
255
316
        """Schedule the contents of a path entry for deletion"""
 
317
        # Ensure that the object exists in the WorkingTree, this will raise an
 
318
        # exception if there is a problem
256
319
        self.tree_kind(trans_id)
257
320
        self._removed_contents.add(trans_id)
258
321
 
1000
1063
class DiskTreeTransform(TreeTransformBase):
1001
1064
    """Tree transform storing its contents on disk."""
1002
1065
 
1003
 
    def __init__(self, tree, limbodir, pb=DummyProgress(),
 
1066
    def __init__(self, tree, limbodir, pb=None,
1004
1067
                 case_sensitive=True):
1005
1068
        """Constructor.
1006
1069
        :param tree: The tree that will be transformed, but not necessarily
1007
1070
            the output tree.
1008
1071
        :param limbodir: A directory where new files can be stored until
1009
1072
            they are installed in their proper places
1010
 
        :param pb: A ProgressBar indicating how much progress is being made
 
1073
        :param pb: ignored
1011
1074
        :param case_sensitive: If True, the target of the transform is
1012
1075
            case sensitive, not just case preserving.
1013
1076
        """
1023
1086
        self._limbo_children_names = {}
1024
1087
        # List of transform ids that need to be renamed from limbo into place
1025
1088
        self._needs_rename = set()
 
1089
        self._creation_mtime = None
1026
1090
 
1027
1091
    def finalize(self):
1028
1092
        """Release the working tree lock, if held, clean up limbo dir.
1075
1139
        if (trans_id in self._limbo_files and
1076
1140
            trans_id not in self._needs_rename):
1077
1141
            self._rename_in_limbo([trans_id])
1078
 
            self._limbo_children[previous_parent].remove(trans_id)
1079
 
            del self._limbo_children_names[previous_parent][previous_name]
 
1142
            if previous_parent != parent:
 
1143
                self._limbo_children[previous_parent].remove(trans_id)
 
1144
            if previous_parent != parent or previous_name != name:
 
1145
                del self._limbo_children_names[previous_parent][previous_name]
1080
1146
 
1081
1147
    def _rename_in_limbo(self, trans_ids):
1082
1148
        """Fix limbo names so that the right final path is produced.
1133
1199
            f.writelines(contents)
1134
1200
        finally:
1135
1201
            f.close()
 
1202
        self._set_mtime(name)
1136
1203
        self._set_mode(trans_id, mode_id, S_ISREG)
1137
1204
 
1138
1205
    def _read_file_chunks(self, trans_id):
1145
1212
    def _read_symlink_target(self, trans_id):
1146
1213
        return os.readlink(self._limbo_name(trans_id))
1147
1214
 
 
1215
    def _set_mtime(self, path):
 
1216
        """All files that are created get the same mtime.
 
1217
 
 
1218
        This time is set by the first object to be created.
 
1219
        """
 
1220
        if self._creation_mtime is None:
 
1221
            self._creation_mtime = time.time()
 
1222
        os.utime(path, (self._creation_mtime, self._creation_mtime))
 
1223
 
1148
1224
    def create_hardlink(self, path, trans_id):
1149
1225
        """Schedule creation of a hard link"""
1150
1226
        name = self._limbo_name(trans_id)
1264
1340
    FileMover does not delete files until it is sure that a rollback will not
1265
1341
    happen.
1266
1342
    """
1267
 
    def __init__(self, tree, pb=DummyProgress()):
 
1343
    def __init__(self, tree, pb=None):
1268
1344
        """Note: a tree_write lock is taken on the tree.
1269
1345
 
1270
1346
        Use TreeTransform.finalize() to release the lock (can be omitted if
1553
1629
                child_pb.update('removing file', num, len(tree_paths))
1554
1630
                full_path = self._tree.abspath(path)
1555
1631
                if trans_id in self._removed_contents:
1556
 
                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
1557
 
                                     trans_id))
1558
 
                elif trans_id in self._new_name or trans_id in \
1559
 
                    self._new_parent:
 
1632
                    delete_path = os.path.join(self._deletiondir, trans_id)
 
1633
                    mover.pre_delete(full_path, delete_path)
 
1634
                elif (trans_id in self._new_name
 
1635
                      or trans_id in self._new_parent):
1560
1636
                    try:
1561
1637
                        mover.rename(full_path, self._limbo_name(trans_id))
1562
1638
                    except OSError, e:
1616
1692
    unversioned files in the input tree.
1617
1693
    """
1618
1694
 
1619
 
    def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
 
1695
    def __init__(self, tree, pb=None, case_sensitive=True):
1620
1696
        tree.lock_read()
1621
1697
        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1622
1698
        DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1973
2049
                statval = os.lstat(limbo_name)
1974
2050
                size = statval.st_size
1975
2051
                if not supports_executable():
1976
 
                    executable = None
 
2052
                    executable = False
1977
2053
                else:
1978
2054
                    executable = statval.st_mode & S_IEXEC
1979
2055
            else:
1981
2057
                executable = None
1982
2058
            if kind == 'symlink':
1983
2059
                link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
1984
 
        if supports_executable():
1985
 
            executable = tt._new_executability.get(trans_id, executable)
 
2060
        executable = tt._new_executability.get(trans_id, executable)
1986
2061
        return kind, size, executable, link_or_sha1
1987
2062
 
1988
2063
    def iter_changes(self, from_tree, include_unchanged=False,
2301
2376
        new_desired_files = desired_files
2302
2377
    else:
2303
2378
        iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2304
 
        unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
2305
 
                         in iter if not (c or e[0] != e[1]))
 
2379
        unchanged = [(f, p[1]) for (f, p, c, v, d, n, k, e)
 
2380
                     in iter if not (c or e[0] != e[1])]
 
2381
        if accelerator_tree.supports_content_filtering():
 
2382
            unchanged = [(f, p) for (f, p) in unchanged
 
2383
                         if not accelerator_tree.iter_search_rules([p]).next()]
 
2384
        unchanged = dict(unchanged)
2306
2385
        new_desired_files = []
2307
2386
        count = 0
2308
2387
        for file_id, (trans_id, tree_path) in desired_files:
2431
2510
        tt.create_directory(trans_id)
2432
2511
 
2433
2512
 
2434
 
def create_from_tree(tt, trans_id, tree, file_id, bytes=None):
2435
 
    """Create new file contents according to tree contents."""
 
2513
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
 
2514
    filter_tree_path=None):
 
2515
    """Create new file contents according to tree contents.
 
2516
    
 
2517
    :param filter_tree_path: the tree path to use to lookup
 
2518
      content filters to apply to the bytes output in the working tree.
 
2519
      This only applies if the working tree supports content filtering.
 
2520
    """
2436
2521
    kind = tree.kind(file_id)
2437
2522
    if kind == 'directory':
2438
2523
        tt.create_directory(trans_id)
2443
2528
                bytes = tree_file.readlines()
2444
2529
            finally:
2445
2530
                tree_file.close()
 
2531
        wt = tt._tree
 
2532
        if wt.supports_content_filtering() and filter_tree_path is not None:
 
2533
            filters = wt._content_filter_stack(filter_tree_path)
 
2534
            bytes = filtered_output_bytes(bytes, filters,
 
2535
                ContentFilterContext(filter_tree_path, tree))
2446
2536
        tt.create_file(bytes, trans_id)
2447
2537
    elif kind == "symlink":
2448
2538
        tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2500
2590
 
2501
2591
 
2502
2592
def revert(working_tree, target_tree, filenames, backups=False,
2503
 
           pb=DummyProgress(), change_reporter=None):
 
2593
           pb=None, change_reporter=None):
2504
2594
    """Revert a working tree's contents to those of a target tree."""
2505
2595
    target_tree.lock_read()
 
2596
    pb = ui.ui_factory.nested_progress_bar()
2506
2597
    tt = TreeTransform(working_tree, pb)
2507
2598
    try:
2508
2599
        pp = ProgressPhase("Revert phase", 3, pb)
2527
2618
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2528
2619
                              backups, pp, basis_tree=None,
2529
2620
                              merge_modified=None):
2530
 
    pp.next_phase()
2531
2621
    child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2532
2622
    try:
2533
2623
        if merge_modified is None:
2537
2627
                                      merge_modified, basis_tree)
2538
2628
    finally:
2539
2629
        child_pb.finished()
2540
 
    pp.next_phase()
2541
2630
    child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2542
2631
    try:
2543
2632
        raw_conflicts = resolve_conflicts(tt, child_pb,
2636
2725
                    parent_trans = ROOT_PARENT
2637
2726
                else:
2638
2727
                    parent_trans = tt.trans_id_file_id(parent[1])
2639
 
                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)
2640
2732
            if executable[0] != executable[1] and kind[1] == "file":
2641
2733
                tt.set_executability(executable[1], trans_id)
2642
2734
        if working_tree.supports_content_filtering():
2655
2747
            for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2656
2748
                deferred_files):
2657
2749
                tt.create_file(bytes, trans_id, mode_id)
 
2750
        tt.fixup_new_roots()
2658
2751
    finally:
2659
2752
        if basis_tree is not None:
2660
2753
            basis_tree.unlock()
2661
2754
    return merge_modified
2662
2755
 
2663
2756
 
2664
 
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
 
2757
def resolve_conflicts(tt, pb=None, pass_func=None):
2665
2758
    """Make many conflict-resolution attempts, but die if they fail"""
2666
2759
    if pass_func is None:
2667
2760
        pass_func = conflict_pass
2668
2761
    new_conflicts = set()
 
2762
    pb = ui.ui_factory.nested_progress_bar()
2669
2763
    try:
2670
2764
        for n in range(10):
2671
2765
            pb.update('Resolution pass', n+1, 10)
2675
2769
            new_conflicts.update(pass_func(tt, conflicts))
2676
2770
        raise MalformedTransform(conflicts=conflicts)
2677
2771
    finally:
2678
 
        pb.clear()
 
2772
        pb.finished()
2679
2773
 
2680
2774
 
2681
2775
def conflict_pass(tt, conflicts, path_tree=None):