~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Martin Pool
  • Date: 2005-09-07 23:14:30 UTC
  • mto: (1092.2.12) (974.1.76) (1185.8.2)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: mbp@sourcefrog.net-20050907231430-097abbaee94ad75b
- docstring fix from Magnus Therning

Show diffs side-by-side

added added

removed removed

Lines of Context:
13
13
#    You should have received a copy of the GNU General Public License
14
14
#    along with this program; if not, write to the Free Software
15
15
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Represent and apply a changeset.
18
 
 
19
 
Conflicts in applying a changeset are represented as exceptions.
20
 
 
21
 
This only handles the in-memory objects representing changesets, which are
22
 
primarily used by the merge code. 
23
 
"""
24
 
 
25
16
import os.path
26
17
import errno
 
18
import patch
27
19
import stat
28
 
from tempfile import mkdtemp
29
 
from shutil import rmtree
30
 
from itertools import izip
31
 
 
32
 
from bzrlib.trace import mutter, warning
33
 
from bzrlib.osutils import rename, sha_file
34
 
import bzrlib
35
 
from bzrlib.errors import BzrCheckError
 
20
from bzrlib.trace import mutter
 
21
 
 
22
# XXX: mbp: I'm not totally convinced that we should handle conflicts
 
23
# as part of changeset application, rather than only in the merge
 
24
# operation.
 
25
 
 
26
"""Represent and apply a changeset
 
27
 
 
28
Conflicts in applying a changeset are represented as exceptions.
 
29
"""
36
30
 
37
31
__docformat__ = "restructuredtext"
38
32
 
48
42
        newdict[value] = key
49
43
    return newdict
50
44
 
 
45
 
51
46
       
52
 
class ChangeExecFlag(object):
 
47
class ChangeUnixPermissions(object):
53
48
    """This is two-way change, suitable for file modification, creation,
54
49
    deletion"""
55
 
    def __init__(self, old_exec_flag, new_exec_flag):
56
 
        self.old_exec_flag = old_exec_flag
57
 
        self.new_exec_flag = new_exec_flag
 
50
    def __init__(self, old_mode, new_mode):
 
51
        self.old_mode = old_mode
 
52
        self.new_mode = new_mode
58
53
 
59
54
    def apply(self, filename, conflict_handler, reverse=False):
60
55
        if not reverse:
61
 
            from_exec_flag = self.old_exec_flag
62
 
            to_exec_flag = self.new_exec_flag
 
56
            from_mode = self.old_mode
 
57
            to_mode = self.new_mode
63
58
        else:
64
 
            from_exec_flag = self.new_exec_flag
65
 
            to_exec_flag = self.old_exec_flag
 
59
            from_mode = self.new_mode
 
60
            to_mode = self.old_mode
66
61
        try:
67
 
            current_exec_flag = bool(os.stat(filename).st_mode & 0111)
 
62
            current_mode = os.stat(filename).st_mode &0777
68
63
        except OSError, e:
69
64
            if e.errno == errno.ENOENT:
70
 
                if conflict_handler.missing_for_exec_flag(filename) == "skip":
 
65
                if conflict_handler.missing_for_chmod(filename) == "skip":
71
66
                    return
72
67
                else:
73
 
                    current_exec_flag = from_exec_flag
 
68
                    current_mode = from_mode
74
69
 
75
 
        if from_exec_flag is not None and current_exec_flag != from_exec_flag:
76
 
            if conflict_handler.wrong_old_exec_flag(filename,
77
 
                        from_exec_flag, current_exec_flag) != "continue":
 
70
        if from_mode is not None and current_mode != from_mode:
 
71
            if conflict_handler.wrong_old_perms(filename, from_mode, 
 
72
                                                current_mode) != "continue":
78
73
                return
79
74
 
80
 
        if to_exec_flag is not None:
81
 
            current_mode = os.stat(filename).st_mode
82
 
            if to_exec_flag:
83
 
                umask = os.umask(0)
84
 
                os.umask(umask)
85
 
                to_mode = current_mode | (0100 & ~umask)
86
 
                # Enable x-bit for others only if they can read it.
87
 
                if current_mode & 0004:
88
 
                    to_mode |= 0001 & ~umask
89
 
                if current_mode & 0040:
90
 
                    to_mode |= 0010 & ~umask
91
 
            else:
92
 
                to_mode = current_mode & ~0111
 
75
        if to_mode is not None:
93
76
            try:
94
77
                os.chmod(filename, to_mode)
95
78
            except IOError, e:
96
79
                if e.errno == errno.ENOENT:
97
 
                    conflict_handler.missing_for_exec_flag(filename)
 
80
                    conflict_handler.missing_for_chmod(filename)
98
81
 
99
82
    def __eq__(self, other):
100
 
        return (isinstance(other, ChangeExecFlag) and
101
 
                self.old_exec_flag == other.old_exec_flag and
102
 
                self.new_exec_flag == other.new_exec_flag)
 
83
        if not isinstance(other, ChangeUnixPermissions):
 
84
            return False
 
85
        elif self.old_mode != other.old_mode:
 
86
            return False
 
87
        elif self.new_mode != other.new_mode:
 
88
            return False
 
89
        else:
 
90
            return True
103
91
 
104
92
    def __ne__(self, other):
105
93
        return not (self == other)
106
94
 
107
 
 
108
95
def dir_create(filename, conflict_handler, reverse):
109
96
    """Creates the directory, or deletes it if reverse is true.  Intended to be
110
97
    used with ReplaceContents.
130
117
        try:
131
118
            os.rmdir(filename)
132
119
        except OSError, e:
133
 
            if e.errno != errno.ENOTEMPTY:
 
120
            if e.errno != 39:
134
121
                raise
135
122
            if conflict_handler.rmdir_non_empty(filename) == "skip":
136
123
                return
137
124
            os.rmdir(filename)
138
125
 
 
126
                
 
127
            
139
128
 
140
129
class SymlinkCreate(object):
141
130
    """Creates or deletes a symlink (for use with ReplaceContents)"""
147
136
        """
148
137
        self.target = contents
149
138
 
150
 
    def __repr__(self):
151
 
        return "SymlinkCreate(%s)" % self.target
152
 
 
153
139
    def __call__(self, filename, conflict_handler, reverse):
154
140
        """Creates or destroys the symlink.
155
141
 
237
223
 
238
224
                    
239
225
 
240
 
class TreeFileCreate(object):
241
 
    """Create or delete a file (for use with ReplaceContents)"""
242
 
    def __init__(self, tree, file_id):
243
 
        """Constructor
244
 
 
245
 
        :param contents: The contents of the file to write
246
 
        :type contents: str
247
 
        """
248
 
        self.tree = tree
249
 
        self.file_id = file_id
250
 
 
251
 
    def __repr__(self):
252
 
        return "TreeFileCreate(%s)" % self.file_id
253
 
 
254
 
    def __eq__(self, other):
255
 
        if not isinstance(other, TreeFileCreate):
256
 
            return False
257
 
        return self.tree.get_file_sha1(self.file_id) == \
258
 
            other.tree.get_file_sha1(other.file_id)
259
 
 
260
 
    def __ne__(self, other):
261
 
        return not (self == other)
262
 
 
263
 
    def write_file(self, filename):
264
 
        outfile = file(filename, "wb")
265
 
        for line in self.tree.get_file(self.file_id):
266
 
            outfile.write(line)
267
 
 
268
 
    def same_text(self, filename):
269
 
        in_file = file(filename, "rb")
270
 
        return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
271
 
 
272
 
    def __call__(self, filename, conflict_handler, reverse):
273
 
        """Create or delete a file
274
 
 
275
 
        :param filename: The name of the file to create
276
 
        :type filename: str
277
 
        :param reverse: Delete the file instead of creating it
278
 
        :type reverse: bool
279
 
        """
280
 
        if not reverse:
281
 
            try:
282
 
                self.write_file(filename)
283
 
            except IOError, e:
284
 
                if e.errno == errno.ENOENT:
285
 
                    if conflict_handler.missing_parent(filename)=="continue":
286
 
                        self.write_file(filename)
287
 
                else:
288
 
                    raise
289
 
 
290
 
        else:
291
 
            try:
292
 
                if not self.same_text(filename):
293
 
                    direction = conflict_handler.wrong_old_contents(filename,
294
 
                        self.tree.get_file(self.file_id).read())
295
 
                    if  direction != "continue":
296
 
                        return
297
 
                os.unlink(filename)
298
 
            except IOError, e:
299
 
                if e.errno != errno.ENOENT:
300
 
                    raise
301
 
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
302
 
                    return
303
 
 
304
 
                    
305
 
 
306
226
def reversed(sequence):
307
227
    max = len(sequence) - 1
308
228
    for i in range(len(sequence)):
375
295
            if mode is not None:
376
296
                os.chmod(filename, mode)
377
297
 
378
 
    def is_creation(self):
379
 
        return self.new_contents is not None and self.old_contents is None
380
 
 
381
 
    def is_deletion(self):
382
 
        return self.old_contents is not None and self.new_contents is None
383
 
 
384
298
class ApplySequence(object):
385
299
    def __init__(self, changes=None):
386
300
        self.changes = []
412
326
 
413
327
 
414
328
class Diff3Merge(object):
415
 
    history_based = False
416
329
    def __init__(self, file_id, base, other):
417
330
        self.file_id = file_id
418
331
        self.base = base
419
332
        self.other = other
420
333
 
421
 
    def is_creation(self):
422
 
        return False
423
 
 
424
 
    def is_deletion(self):
425
 
        return False
426
 
 
427
334
    def __eq__(self, other):
428
335
        if not isinstance(other, Diff3Merge):
429
336
            return False
433
340
    def __ne__(self, other):
434
341
        return not (self == other)
435
342
 
436
 
    def dump_file(self, temp_dir, name, tree):
437
 
        out_path = os.path.join(temp_dir, name)
438
 
        out_file = file(out_path, "wb")
439
 
        in_file = tree.get_file(self.file_id)
440
 
        for line in in_file:
441
 
            out_file.write(line)
442
 
        return out_path
443
 
 
444
343
    def apply(self, filename, conflict_handler, reverse=False):
445
 
        import bzrlib.patch
446
 
        temp_dir = mkdtemp(prefix="bzr-")
447
 
        try:
448
 
            new_file = filename+".new"
449
 
            base_file = self.dump_file(temp_dir, "base", self.base)
450
 
            other_file = self.dump_file(temp_dir, "other", self.other)
451
 
            if not reverse:
452
 
                base = base_file
453
 
                other = other_file
454
 
            else:
455
 
                base = other_file
456
 
                other = base_file
457
 
            status = bzrlib.patch.diff3(new_file, filename, base, other)
458
 
            if status == 0:
459
 
                os.chmod(new_file, os.stat(filename).st_mode)
460
 
                rename(new_file, filename)
461
 
                return
462
 
            else:
463
 
                assert(status == 1)
464
 
                def get_lines(filename):
465
 
                    my_file = file(filename, "rb")
466
 
                    lines = my_file.readlines()
467
 
                    my_file.close()
468
 
                    return lines
469
 
                base_lines = get_lines(base)
470
 
                other_lines = get_lines(other)
471
 
                conflict_handler.merge_conflict(new_file, filename, base_lines, 
472
 
                                                other_lines)
473
 
        finally:
474
 
            rmtree(temp_dir)
 
344
        new_file = filename+".new"
 
345
        base_file = self.base.readonly_path(self.file_id)
 
346
        other_file = self.other.readonly_path(self.file_id)
 
347
        if not reverse:
 
348
            base = base_file
 
349
            other = other_file
 
350
        else:
 
351
            base = other_file
 
352
            other = base_file
 
353
        status = patch.diff3(new_file, filename, base, other)
 
354
        if status == 0:
 
355
            os.chmod(new_file, os.stat(filename).st_mode)
 
356
            os.rename(new_file, filename)
 
357
            return
 
358
        else:
 
359
            assert(status == 1)
 
360
            def get_lines(filename):
 
361
                my_file = file(base, "rb")
 
362
                lines = my_file.readlines()
 
363
                my_file.close()
 
364
            base_lines = get_lines(base)
 
365
            other_lines = get_lines(other)
 
366
            conflict_handler.merge_conflict(new_file, filename, base_lines, 
 
367
                                            other_lines)
475
368
 
476
369
 
477
370
def CreateDir():
510
403
    """
511
404
    return ReplaceContents(FileCreate(contents), None)
512
405
 
513
 
def ReplaceFileContents(old_tree, new_tree, file_id):
 
406
def ReplaceFileContents(old_contents, new_contents):
514
407
    """Convenience fucntion to replace the contents of a file.
515
408
    
516
409
    :param old_contents: The contents of the file to replace 
520
413
    :return: A ReplaceContents that will replace the contents of a file a file 
521
414
    :rtype: `ReplaceContents`
522
415
    """
523
 
    return ReplaceContents(TreeFileCreate(old_tree, file_id), 
524
 
                           TreeFileCreate(new_tree, file_id))
 
416
    return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
525
417
 
526
418
def CreateSymlink(target):
527
419
    """Convenience fucntion to create a symlink.
633
525
        if self.id  == self.parent:
634
526
            raise ParentIDIsSelf(self)
635
527
 
636
 
    def __repr__(self):
 
528
    def __str__(self):
637
529
        return "ChangesetEntry(%s)" % self.id
638
530
 
639
 
    __str__ = __repr__
640
 
 
641
531
    def __get_dir(self):
642
532
        if self.path is None:
643
533
            return None
692
582
        :param reverse: if true, the changeset is being applied in reverse
693
583
        :rtype: bool
694
584
        """
695
 
        return self.is_creation(not reverse)
 
585
        return ((self.new_parent is None and not reverse) or 
 
586
                (self.parent is None and reverse))
696
587
 
697
588
    def is_creation(self, reverse):
698
589
        """Return true if applying the entry would create a file/directory.
700
591
        :param reverse: if true, the changeset is being applied in reverse
701
592
        :rtype: bool
702
593
        """
703
 
        if self.contents_change is None:
704
 
            return False
705
 
        if reverse:
706
 
            return self.contents_change.is_deletion()
707
 
        else:
708
 
            return self.contents_change.is_creation()
 
594
        return ((self.parent is None and not reverse) or 
 
595
                (self.new_parent is None and reverse))
709
596
 
710
597
    def is_creation_or_deletion(self):
711
598
        """Return true if applying the entry would create or delete a 
713
600
 
714
601
        :rtype: bool
715
602
        """
716
 
        return self.is_creation(False) or self.is_deletion(False)
 
603
        return self.parent is None or self.new_parent is None
717
604
 
718
605
    def get_cset_path(self, mod=False):
719
606
        """Determine the path of the entry according to the changeset.
752
639
        """
753
640
        orig_path = self.get_cset_path(False)
754
641
        mod_path = self.get_cset_path(True)
755
 
        if orig_path and orig_path.startswith('./'):
 
642
        if orig_path is not None:
756
643
            orig_path = orig_path[2:]
757
 
        if mod_path and mod_path.startswith('./'):
 
644
        if mod_path is not None:
758
645
            mod_path = mod_path[2:]
759
646
        if orig_path == mod_path:
760
647
            return orig_path
776
663
        :type reverse: bool
777
664
        :rtype: str
778
665
        """
779
 
        mutter("Finding new path for %s", self.summarize_name())
 
666
        mutter("Finding new path for %s" % self.summarize_name())
780
667
        if reverse:
781
668
            parent = self.parent
782
669
            to_dir = self.dir
801
688
        if from_dir == to_dir:
802
689
            dir = os.path.dirname(id_map[self.id])
803
690
        else:
804
 
            mutter("path, new_path: %r %r", self.path, self.new_path)
 
691
            mutter("path, new_path: %r %r" % (self.path, self.new_path))
805
692
            parent_entry = changeset.entries[parent]
806
693
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
807
694
        if from_name == to_name:
891
778
    :rtype: (List, List)
892
779
    """
893
780
    source_entries = [x for x in changeset.entries.itervalues() 
894
 
                      if x.needs_rename() or x.is_creation_or_deletion()]
 
781
                      if x.needs_rename()]
895
782
    # these are done from longest path to shortest, to avoid deleting a
896
783
    # parent before its children are deleted/renamed 
897
784
    def longest_to_shortest(entry):
938
825
            entry.apply(path, conflict_handler, reverse)
939
826
            temp_name[entry.id] = None
940
827
 
941
 
        elif entry.needs_rename():
 
828
        else:
942
829
            to_name = os.path.join(temp_dir, str(i))
943
830
            src_path = inventory.get(entry.id)
944
831
            if src_path is not None:
945
832
                src_path = os.path.join(dir, src_path)
946
833
                try:
947
 
                    rename(src_path, to_name)
 
834
                    os.rename(src_path, to_name)
948
835
                    temp_name[entry.id] = to_name
949
836
                except OSError, e:
950
837
                    if e.errno != errno.ENOENT:
951
838
                        raise
952
 
                    if conflict_handler.missing_for_rename(src_path, to_name) \
953
 
                        == "skip":
 
839
                    if conflict_handler.missing_for_rename(src_path) == "skip":
954
840
                        continue
955
841
 
956
842
    return temp_name
977
863
            continue
978
864
        new_path = os.path.join(dir, new_tree_path)
979
865
        old_path = changed_inventory.get(entry.id)
980
 
        if bzrlib.osutils.lexists(new_path):
 
866
        if os.path.exists(new_path):
981
867
            if conflict_handler.target_exists(entry, new_path, old_path) == \
982
868
                "skip":
983
869
                continue
984
870
        if entry.is_creation(reverse):
985
871
            entry.apply(new_path, conflict_handler, reverse)
986
872
            changed_inventory[entry.id] = new_tree_path
987
 
        elif entry.needs_rename():
 
873
        else:
988
874
            if old_path is None:
989
875
                continue
990
876
            try:
991
 
                mutter('rename %s to final name %s', old_path, new_path)
992
 
                rename(old_path, new_path)
 
877
                os.rename(old_path, new_path)
993
878
                changed_inventory[entry.id] = new_tree_path
994
879
            except OSError, e:
995
 
                raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
996
 
                        % (old_path, new_path, entry, e))
 
880
                raise Exception ("%s is missing" % new_path)
997
881
 
998
882
class TargetExists(Exception):
999
883
    def __init__(self, entry, target):
1031
915
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
1032
916
        self.this_path = this_path
1033
917
 
 
918
class MergePermissionConflict(Exception):
 
919
    def __init__(self, this_path, base_path, other_path):
 
920
        this_perms = os.stat(this_path).st_mode & 0755
 
921
        base_perms = os.stat(base_path).st_mode & 0755
 
922
        other_perms = os.stat(other_path).st_mode & 0755
 
923
        msg = """Conflicting permission for %s
 
924
this: %o
 
925
base: %o
 
926
other: %o
 
927
        """ % (this_path, this_perms, base_perms, other_perms)
 
928
        self.this_path = this_path
 
929
        self.base_path = base_path
 
930
        self.other_path = other_path
 
931
        Exception.__init__(self, msg)
 
932
 
1034
933
class WrongOldContents(Exception):
1035
934
    def __init__(self, filename):
1036
935
        msg = "Contents mismatch deleting %s" % filename
1037
936
        self.filename = filename
1038
937
        Exception.__init__(self, msg)
1039
938
 
1040
 
class WrongOldExecFlag(Exception):
1041
 
    def __init__(self, filename, old_exec_flag, new_exec_flag):
1042
 
        msg = "Executable flag missmatch on %s:\n" \
1043
 
        "Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
 
939
class WrongOldPermissions(Exception):
 
940
    def __init__(self, filename, old_perms, new_perms):
 
941
        msg = "Permission missmatch on %s:\n" \
 
942
        "Expected 0%o, got 0%o." % (filename, old_perms, new_perms)
1044
943
        self.filename = filename
1045
944
        Exception.__init__(self, msg)
1046
945
 
1064
963
        Exception.__init__(self, msg)
1065
964
        self.filename = filename
1066
965
 
1067
 
class MissingForSetExec(Exception):
 
966
class MissingPermsFile(Exception):
1068
967
    def __init__(self, filename):
1069
968
        msg = "Attempt to change permissions on  %s, which does not exist" %\
1070
969
            filename
1079
978
 
1080
979
 
1081
980
class MissingForRename(Exception):
1082
 
    def __init__(self, filename, to_path):
1083
 
        msg = "Attempt to move missing path %s to %s" % (filename, to_path)
 
981
    def __init__(self, filename):
 
982
        msg = "Attempt to move missing path %s" % (filename)
1084
983
        Exception.__init__(self, msg)
1085
984
        self.filename = filename
1086
985
 
1089
988
        msg = "Conflicting contents for new file %s" % (filename)
1090
989
        Exception.__init__(self, msg)
1091
990
 
1092
 
class WeaveMergeConflict(Exception):
1093
 
    def __init__(self, filename):
1094
 
        msg = "Conflicting contents for file %s" % (filename)
1095
 
        Exception.__init__(self, msg)
1096
 
 
1097
 
class ThreewayContentsConflict(Exception):
1098
 
    def __init__(self, filename):
1099
 
        msg = "Conflicting contents for file %s" % (filename)
1100
 
        Exception.__init__(self, msg)
1101
 
 
1102
991
 
1103
992
class MissingForMerge(Exception):
1104
993
    def __init__(self, filename):
1114
1003
    descend from this class if they have a better way to handle some or
1115
1004
    all types of conflict.
1116
1005
    """
 
1006
    def __init__(self, dir):
 
1007
        self.dir = dir
 
1008
    
1117
1009
    def missing_parent(self, pathname):
1118
1010
        parent = os.path.dirname(pathname)
1119
1011
        raise Exception("Parent directory missing for %s" % pathname)
1137
1029
        os.unlink(new_file)
1138
1030
        raise MergeConflict(this_path)
1139
1031
 
 
1032
    def permission_conflict(self, this_path, base_path, other_path):
 
1033
        raise MergePermissionConflict(this_path, base_path, other_path)
 
1034
 
1140
1035
    def wrong_old_contents(self, filename, expected_contents):
1141
1036
        raise WrongOldContents(filename)
1142
1037
 
1143
1038
    def rem_contents_conflict(self, filename, this_contents, base_contents):
1144
1039
        raise RemoveContentsConflict(filename)
1145
1040
 
1146
 
    def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
1147
 
        raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
 
1041
    def wrong_old_perms(self, filename, old_perms, new_perms):
 
1042
        raise WrongOldPermissions(filename, old_perms, new_perms)
1148
1043
 
1149
1044
    def rmdir_non_empty(self, filename):
1150
1045
        raise DeletingNonEmptyDirectory(filename)
1155
1050
    def patch_target_missing(self, filename, contents):
1156
1051
        raise PatchTargetMissing(filename)
1157
1052
 
1158
 
    def missing_for_exec_flag(self, filename):
1159
 
        raise MissingForExecFlag(filename)
 
1053
    def missing_for_chmod(self, filename):
 
1054
        raise MissingPermsFile(filename)
1160
1055
 
1161
1056
    def missing_for_rm(self, filename, change):
1162
1057
        raise MissingForRm(filename)
1163
1058
 
1164
 
    def missing_for_rename(self, filename, to_path):
1165
 
        raise MissingForRename(filename, to_path)
 
1059
    def missing_for_rename(self, filename):
 
1060
        raise MissingForRename(filename)
1166
1061
 
1167
1062
    def missing_for_merge(self, file_id, other_path):
1168
1063
        raise MissingForMerge(other_path)
1170
1065
    def new_contents_conflict(self, filename, other_contents):
1171
1066
        raise NewContentsConflict(filename)
1172
1067
 
1173
 
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
1174
 
        raise WeaveMergeConflict(filename)
1175
 
 
1176
 
    def threeway_contents_conflict(self, filename, this_contents,
1177
 
                                   base_contents, other_contents):
1178
 
        raise ThreewayContentsConflict(filename)
1179
 
 
1180
1068
    def finalize(self):
1181
1069
        pass
1182
1070
 
1196
1084
    :rtype: Dictionary
1197
1085
    """
1198
1086
    if conflict_handler is None:
1199
 
        conflict_handler = ExceptionConflictHandler()
 
1087
        conflict_handler = ExceptionConflictHandler(dir)
1200
1088
    temp_dir = os.path.join(dir, "bzr-tree-change")
1201
1089
    try:
1202
1090
        os.mkdir(temp_dir)
1213
1101
    
1214
1102
    #apply changes that don't affect filenames
1215
1103
    for entry in changeset.entries.itervalues():
1216
 
        if not entry.is_creation_or_deletion() and not entry.is_boring():
1217
 
            if entry.id not in inventory:
1218
 
                warning("entry {%s} no longer present, can't be updated",
1219
 
                        entry.id)
1220
 
                continue
 
1104
        if not entry.is_creation_or_deletion():
1221
1105
            path = os.path.join(dir, inventory[entry.id])
1222
1106
            entry.apply(path, conflict_handler, reverse)
1223
1107
 
1242
1126
    r_inventory = {}
1243
1127
    for entry in tree.source_inventory().itervalues():
1244
1128
        inventory[entry.id] = entry.path
1245
 
    new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
 
1129
    new_inventory = apply_changeset(cset, r_inventory, tree.root,
1246
1130
                                    reverse=reverse)
1247
1131
    new_entries, remove_entries = \
1248
1132
        get_inventory_change(inventory, new_inventory, cset, reverse)
1383
1267
        return new_meta
1384
1268
    elif new_meta is None:
1385
1269
        return old_meta
1386
 
    elif (isinstance(old_meta, ChangeExecFlag) and
1387
 
          isinstance(new_meta, ChangeExecFlag)):
1388
 
        return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
 
1270
    elif isinstance(old_meta, ChangeUnixPermissions) and \
 
1271
        isinstance(new_meta, ChangeUnixPermissions):
 
1272
        return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1389
1273
    else:
1390
1274
        return ApplySequence(old_meta, new_meta)
1391
1275
 
1396
1280
            return False
1397
1281
    return True
1398
1282
 
1399
 
class UnsupportedFiletype(Exception):
1400
 
    def __init__(self, kind, full_path):
1401
 
        msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1402
 
            % (full_path, kind)
 
1283
class UnsuppportedFiletype(Exception):
 
1284
    def __init__(self, full_path, stat_result):
 
1285
        msg = "The file \"%s\" is not a supported filetype." % full_path
1403
1286
        Exception.__init__(self, msg)
1404
1287
        self.full_path = full_path
1405
 
        self.kind = kind
 
1288
        self.stat_result = stat_result
1406
1289
 
1407
1290
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1408
1291
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1409
1292
 
1410
 
 
1411
1293
class ChangesetGenerator(object):
1412
1294
    def __init__(self, tree_a, tree_b, interesting_ids=None):
1413
1295
        object.__init__(self)
1449
1331
    def get_entry(self, file_id, tree):
1450
1332
        if not tree.has_or_had_id(file_id):
1451
1333
            return None
1452
 
        return tree.inventory[file_id]
 
1334
        return tree.tree.inventory[file_id]
1453
1335
 
1454
1336
    def get_entry_parent(self, entry):
1455
1337
        if entry is None:
1505
1387
 
1506
1388
        if cs_entry is None:
1507
1389
            return None
1508
 
 
1509
 
        cs_entry.metadata_change = self.make_exec_flag_change(id)
1510
 
 
1511
1390
        if id in self.tree_a and id in self.tree_b:
1512
1391
            a_sha1 = self.tree_a.get_file_sha1(id)
1513
1392
            b_sha1 = self.tree_b.get_file_sha1(id)
1514
1393
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1515
1394
                return cs_entry
1516
1395
 
1517
 
        cs_entry.contents_change = self.make_contents_change(id)
 
1396
        full_path_a = self.tree_a.readonly_path(id)
 
1397
        full_path_b = self.tree_b.readonly_path(id)
 
1398
        stat_a = self.lstat(full_path_a)
 
1399
        stat_b = self.lstat(full_path_b)
 
1400
        
 
1401
        cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
 
1402
        cs_entry.contents_change = self.make_contents_change(full_path_a,
 
1403
                                                             stat_a, 
 
1404
                                                             full_path_b, 
 
1405
                                                             stat_b)
1518
1406
        return cs_entry
1519
1407
 
1520
 
    def make_exec_flag_change(self, file_id):
1521
 
        exec_flag_a = exec_flag_b = None
1522
 
        if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1523
 
            exec_flag_a = self.tree_a.is_executable(file_id)
1524
 
 
1525
 
        if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1526
 
            exec_flag_b = self.tree_b.is_executable(file_id)
1527
 
 
1528
 
        if exec_flag_a == exec_flag_b:
1529
 
            return None
1530
 
        return ChangeExecFlag(exec_flag_a, exec_flag_b)
1531
 
 
1532
 
    def make_contents_change(self, file_id):
1533
 
        a_contents = get_contents(self.tree_a, file_id)
1534
 
        b_contents = get_contents(self.tree_b, file_id)
 
1408
    def make_mode_change(self, stat_a, stat_b):
 
1409
        mode_a = None
 
1410
        if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
 
1411
            mode_a = stat_a.st_mode & 0777
 
1412
        mode_b = None
 
1413
        if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
 
1414
            mode_b = stat_b.st_mode & 0777
 
1415
        if mode_a == mode_b:
 
1416
            return None
 
1417
        return ChangeUnixPermissions(mode_a, mode_b)
 
1418
 
 
1419
    def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
 
1420
        if stat_a is None and stat_b is None:
 
1421
            return None
 
1422
        if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
 
1423
            stat.S_ISDIR(stat_b.st_mode):
 
1424
            return None
 
1425
        if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
 
1426
            stat.S_ISREG(stat_b.st_mode):
 
1427
            if stat_a.st_ino == stat_b.st_ino and \
 
1428
                stat_a.st_dev == stat_b.st_dev:
 
1429
                return None
 
1430
 
 
1431
        a_contents = self.get_contents(stat_a, full_path_a)
 
1432
        b_contents = self.get_contents(stat_b, full_path_b)
1535
1433
        if a_contents == b_contents:
1536
1434
            return None
1537
1435
        return ReplaceContents(a_contents, b_contents)
1538
1436
 
 
1437
    def get_contents(self, stat_result, full_path):
 
1438
        if stat_result is None:
 
1439
            return None
 
1440
        elif stat.S_ISREG(stat_result.st_mode):
 
1441
            return FileCreate(file(full_path, "rb").read())
 
1442
        elif stat.S_ISDIR(stat_result.st_mode):
 
1443
            return dir_create
 
1444
        elif stat.S_ISLNK(stat_result.st_mode):
 
1445
            return SymlinkCreate(os.readlink(full_path))
 
1446
        else:
 
1447
            raise UnsupportedFiletype(full_path, stat_result)
1539
1448
 
1540
 
def get_contents(tree, file_id):
1541
 
    """Return the appropriate contents to create a copy of file_id from tree"""
1542
 
    if file_id not in tree:
1543
 
        return None
1544
 
    kind = tree.kind(file_id)
1545
 
    if kind == "file":
1546
 
        return TreeFileCreate(tree, file_id)
1547
 
    elif kind in ("directory", "root_directory"):
1548
 
        return dir_create
1549
 
    elif kind == "symlink":
1550
 
        return SymlinkCreate(tree.get_symlink_target(file_id))
1551
 
    else:
1552
 
        raise UnsupportedFiletype(kind, tree.id2path(file_id))
 
1449
    def lstat(self, full_path):
 
1450
        stat_result = None
 
1451
        if full_path is not None:
 
1452
            try:
 
1453
                stat_result = os.lstat(full_path)
 
1454
            except OSError, e:
 
1455
                if e.errno != errno.ENOENT:
 
1456
                    raise
 
1457
        return stat_result
1553
1458
 
1554
1459
 
1555
1460
def full_path(entry, tree):
1556
 
    return os.path.join(tree.basedir, entry.path)
 
1461
    return os.path.join(tree.root, entry.path)
1557
1462
 
1558
1463
def new_delete_entry(entry, tree, inventory, delete):
1559
1464
    if entry.path == "":