~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Robert Collins
  • Date: 2005-10-16 00:22:17 UTC
  • mto: This revision was merged to the branch mainline in revision 1457.
  • Revision ID: robertc@lifelesslap.robertcollins.net-20051016002217-aa38f9c1eb13ee48
Plugins are now loaded under bzrlib.plugins, not bzrlib.plugin.

Plugins are also made available for other plugins to use by making them 
accessible via import bzrlib.plugins.NAME. You should not import other
plugins during the __init__ of your plugin though, as no ordering is
guaranteed, and the plugins directory is not on the python path.

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
 
20
from bzrlib.trace import mutter
 
21
from bzrlib.osutils import rename
34
22
import bzrlib
35
 
from bzrlib.errors import BzrCheckError
 
23
 
 
24
# XXX: mbp: I'm not totally convinced that we should handle conflicts
 
25
# as part of changeset application, rather than only in the merge
 
26
# operation.
 
27
 
 
28
"""Represent and apply a changeset
 
29
 
 
30
Conflicts in applying a changeset are represented as exceptions.
 
31
"""
36
32
 
37
33
__docformat__ = "restructuredtext"
38
34
 
147
143
        """
148
144
        self.target = contents
149
145
 
150
 
    def __repr__(self):
151
 
        return "SymlinkCreate(%s)" % self.target
152
 
 
153
146
    def __call__(self, filename, conflict_handler, reverse):
154
147
        """Creates or destroys the symlink.
155
148
 
237
230
 
238
231
                    
239
232
 
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
233
def reversed(sequence):
307
234
    max = len(sequence) - 1
308
235
    for i in range(len(sequence)):
375
302
            if mode is not None:
376
303
                os.chmod(filename, mode)
377
304
 
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
305
class ApplySequence(object):
385
306
    def __init__(self, changes=None):
386
307
        self.changes = []
412
333
 
413
334
 
414
335
class Diff3Merge(object):
415
 
    history_based = False
416
336
    def __init__(self, file_id, base, other):
417
337
        self.file_id = file_id
418
338
        self.base = base
419
339
        self.other = other
420
340
 
421
 
    def is_creation(self):
422
 
        return False
423
 
 
424
 
    def is_deletion(self):
425
 
        return False
426
 
 
427
341
    def __eq__(self, other):
428
342
        if not isinstance(other, Diff3Merge):
429
343
            return False
433
347
    def __ne__(self, other):
434
348
        return not (self == other)
435
349
 
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
350
    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)
 
351
        new_file = filename+".new"
 
352
        base_file = self.base.readonly_path(self.file_id)
 
353
        other_file = self.other.readonly_path(self.file_id)
 
354
        if not reverse:
 
355
            base = base_file
 
356
            other = other_file
 
357
        else:
 
358
            base = other_file
 
359
            other = base_file
 
360
        status = patch.diff3(new_file, filename, base, other)
 
361
        if status == 0:
 
362
            os.chmod(new_file, os.stat(filename).st_mode)
 
363
            rename(new_file, filename)
 
364
            return
 
365
        else:
 
366
            assert(status == 1)
 
367
            def get_lines(filename):
 
368
                my_file = file(filename, "rb")
 
369
                lines = my_file.readlines()
 
370
                my_file.close()
 
371
                return lines
 
372
            base_lines = get_lines(base)
 
373
            other_lines = get_lines(other)
 
374
            conflict_handler.merge_conflict(new_file, filename, base_lines, 
 
375
                                            other_lines)
475
376
 
476
377
 
477
378
def CreateDir():
510
411
    """
511
412
    return ReplaceContents(FileCreate(contents), None)
512
413
 
513
 
def ReplaceFileContents(old_tree, new_tree, file_id):
 
414
def ReplaceFileContents(old_contents, new_contents):
514
415
    """Convenience fucntion to replace the contents of a file.
515
416
    
516
417
    :param old_contents: The contents of the file to replace 
520
421
    :return: A ReplaceContents that will replace the contents of a file a file 
521
422
    :rtype: `ReplaceContents`
522
423
    """
523
 
    return ReplaceContents(TreeFileCreate(old_tree, file_id), 
524
 
                           TreeFileCreate(new_tree, file_id))
 
424
    return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
525
425
 
526
426
def CreateSymlink(target):
527
427
    """Convenience fucntion to create a symlink.
633
533
        if self.id  == self.parent:
634
534
            raise ParentIDIsSelf(self)
635
535
 
636
 
    def __repr__(self):
 
536
    def __str__(self):
637
537
        return "ChangesetEntry(%s)" % self.id
638
538
 
639
 
    __str__ = __repr__
640
 
 
641
539
    def __get_dir(self):
642
540
        if self.path is None:
643
541
            return None
692
590
        :param reverse: if true, the changeset is being applied in reverse
693
591
        :rtype: bool
694
592
        """
695
 
        return self.is_creation(not reverse)
 
593
        return ((self.new_parent is None and not reverse) or 
 
594
                (self.parent is None and reverse))
696
595
 
697
596
    def is_creation(self, reverse):
698
597
        """Return true if applying the entry would create a file/directory.
700
599
        :param reverse: if true, the changeset is being applied in reverse
701
600
        :rtype: bool
702
601
        """
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()
 
602
        return ((self.parent is None and not reverse) or 
 
603
                (self.new_parent is None and reverse))
709
604
 
710
605
    def is_creation_or_deletion(self):
711
606
        """Return true if applying the entry would create or delete a 
713
608
 
714
609
        :rtype: bool
715
610
        """
716
 
        return self.is_creation(False) or self.is_deletion(False)
 
611
        return self.parent is None or self.new_parent is None
717
612
 
718
613
    def get_cset_path(self, mod=False):
719
614
        """Determine the path of the entry according to the changeset.
752
647
        """
753
648
        orig_path = self.get_cset_path(False)
754
649
        mod_path = self.get_cset_path(True)
755
 
        if orig_path and orig_path.startswith('./'):
 
650
        if orig_path is not None:
756
651
            orig_path = orig_path[2:]
757
 
        if mod_path and mod_path.startswith('./'):
 
652
        if mod_path is not None:
758
653
            mod_path = mod_path[2:]
759
654
        if orig_path == mod_path:
760
655
            return orig_path
776
671
        :type reverse: bool
777
672
        :rtype: str
778
673
        """
779
 
        mutter("Finding new path for %s", self.summarize_name())
 
674
        mutter("Finding new path for %s" % self.summarize_name())
780
675
        if reverse:
781
676
            parent = self.parent
782
677
            to_dir = self.dir
798
693
                raise SourceRootHasName(self, to_name)
799
694
            else:
800
695
                return '.'
801
 
        parent_entry = changeset.entries.get(parent)
802
 
        if parent_entry is None:
 
696
        if from_dir == to_dir:
803
697
            dir = os.path.dirname(id_map[self.id])
804
698
        else:
805
 
            mutter("path, new_path: %r %r", self.path, self.new_path)
 
699
            mutter("path, new_path: %r %r" % (self.path, self.new_path))
 
700
            parent_entry = changeset.entries[parent]
806
701
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
807
702
        if from_name == to_name:
808
703
            name = os.path.basename(id_map[self.id])
891
786
    :rtype: (List, List)
892
787
    """
893
788
    source_entries = [x for x in changeset.entries.itervalues() 
894
 
                      if x.needs_rename() or x.is_creation_or_deletion()]
 
789
                      if x.needs_rename()]
895
790
    # these are done from longest path to shortest, to avoid deleting a
896
791
    # parent before its children are deleted/renamed 
897
792
    def longest_to_shortest(entry):
938
833
            entry.apply(path, conflict_handler, reverse)
939
834
            temp_name[entry.id] = None
940
835
 
941
 
        elif entry.needs_rename():
942
 
            if entry.is_creation(reverse):
943
 
                continue
 
836
        else:
944
837
            to_name = os.path.join(temp_dir, str(i))
945
838
            src_path = inventory.get(entry.id)
946
839
            if src_path is not None:
951
844
                except OSError, e:
952
845
                    if e.errno != errno.ENOENT:
953
846
                        raise
954
 
                    if conflict_handler.missing_for_rename(src_path, to_name) \
955
 
                        == "skip":
 
847
                    if conflict_handler.missing_for_rename(src_path) == "skip":
956
848
                        continue
957
849
 
958
850
    return temp_name
986
878
        if entry.is_creation(reverse):
987
879
            entry.apply(new_path, conflict_handler, reverse)
988
880
            changed_inventory[entry.id] = new_tree_path
989
 
        elif entry.needs_rename():
990
 
            if entry.is_deletion(reverse):
991
 
                continue
 
881
        else:
992
882
            if old_path is None:
993
883
                continue
994
884
            try:
995
 
                mutter('rename %s to final name %s', old_path, new_path)
996
885
                rename(old_path, new_path)
997
886
                changed_inventory[entry.id] = new_tree_path
998
887
            except OSError, e:
999
 
                raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
1000
 
                        % (old_path, new_path, entry, e))
 
888
                raise Exception ("%s is missing" % new_path)
1001
889
 
1002
890
class TargetExists(Exception):
1003
891
    def __init__(self, entry, target):
1083
971
 
1084
972
 
1085
973
class MissingForRename(Exception):
1086
 
    def __init__(self, filename, to_path):
1087
 
        msg = "Attempt to move missing path %s to %s" % (filename, to_path)
 
974
    def __init__(self, filename):
 
975
        msg = "Attempt to move missing path %s" % (filename)
1088
976
        Exception.__init__(self, msg)
1089
977
        self.filename = filename
1090
978
 
1093
981
        msg = "Conflicting contents for new file %s" % (filename)
1094
982
        Exception.__init__(self, msg)
1095
983
 
1096
 
class WeaveMergeConflict(Exception):
1097
 
    def __init__(self, filename):
1098
 
        msg = "Conflicting contents for file %s" % (filename)
1099
 
        Exception.__init__(self, msg)
1100
 
 
1101
 
class ThreewayContentsConflict(Exception):
1102
 
    def __init__(self, filename):
1103
 
        msg = "Conflicting contents for file %s" % (filename)
1104
 
        Exception.__init__(self, msg)
1105
 
 
1106
984
 
1107
985
class MissingForMerge(Exception):
1108
986
    def __init__(self, filename):
1165
1043
    def missing_for_rm(self, filename, change):
1166
1044
        raise MissingForRm(filename)
1167
1045
 
1168
 
    def missing_for_rename(self, filename, to_path):
1169
 
        raise MissingForRename(filename, to_path)
 
1046
    def missing_for_rename(self, filename):
 
1047
        raise MissingForRename(filename)
1170
1048
 
1171
1049
    def missing_for_merge(self, file_id, other_path):
1172
1050
        raise MissingForMerge(other_path)
1174
1052
    def new_contents_conflict(self, filename, other_contents):
1175
1053
        raise NewContentsConflict(filename)
1176
1054
 
1177
 
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
1178
 
        raise WeaveMergeConflict(filename)
1179
 
 
1180
 
    def threeway_contents_conflict(self, filename, this_contents,
1181
 
                                   base_contents, other_contents):
1182
 
        raise ThreewayContentsConflict(filename)
1183
 
 
1184
1055
    def finalize(self):
1185
1056
        pass
1186
1057
 
1217
1088
    
1218
1089
    #apply changes that don't affect filenames
1219
1090
    for entry in changeset.entries.itervalues():
1220
 
        if not entry.is_creation_or_deletion() and not entry.is_boring():
1221
 
            if entry.id not in inventory:
1222
 
                warning("entry {%s} no longer present, can't be updated",
1223
 
                        entry.id)
1224
 
                continue
 
1091
        if not entry.is_creation_or_deletion():
1225
1092
            path = os.path.join(dir, inventory[entry.id])
1226
1093
            entry.apply(path, conflict_handler, reverse)
1227
1094
 
1246
1113
    r_inventory = {}
1247
1114
    for entry in tree.source_inventory().itervalues():
1248
1115
        inventory[entry.id] = entry.path
1249
 
    new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
 
1116
    new_inventory = apply_changeset(cset, r_inventory, tree.root,
1250
1117
                                    reverse=reverse)
1251
1118
    new_entries, remove_entries = \
1252
1119
        get_inventory_change(inventory, new_inventory, cset, reverse)
1400
1267
            return False
1401
1268
    return True
1402
1269
 
1403
 
class UnsupportedFiletype(Exception):
1404
 
    def __init__(self, kind, full_path):
1405
 
        msg = "The file \"%s\" is a %s, which is not a supported filetype." \
1406
 
            % (full_path, kind)
 
1270
class UnsuppportedFiletype(Exception):
 
1271
    def __init__(self, full_path, stat_result):
 
1272
        msg = "The file \"%s\" is not a supported filetype." % full_path
1407
1273
        Exception.__init__(self, msg)
1408
1274
        self.full_path = full_path
1409
 
        self.kind = kind
 
1275
        self.stat_result = stat_result
1410
1276
 
1411
1277
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1412
1278
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1453
1319
    def get_entry(self, file_id, tree):
1454
1320
        if not tree.has_or_had_id(file_id):
1455
1321
            return None
1456
 
        return tree.inventory[file_id]
 
1322
        return tree.tree.inventory[file_id]
1457
1323
 
1458
1324
    def get_entry_parent(self, entry):
1459
1325
        if entry is None:
1510
1376
        if cs_entry is None:
1511
1377
            return None
1512
1378
 
1513
 
        cs_entry.metadata_change = self.make_exec_flag_change(id)
 
1379
        full_path_a = self.tree_a.readonly_path(id)
 
1380
        full_path_b = self.tree_b.readonly_path(id)
 
1381
        stat_a = self.lstat(full_path_a)
 
1382
        stat_b = self.lstat(full_path_b)
 
1383
 
 
1384
        cs_entry.metadata_change = self.make_exec_flag_change(stat_a, stat_b)
1514
1385
 
1515
1386
        if id in self.tree_a and id in self.tree_b:
1516
1387
            a_sha1 = self.tree_a.get_file_sha1(id)
1518
1389
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1519
1390
                return cs_entry
1520
1391
 
1521
 
        cs_entry.contents_change = self.make_contents_change(id)
 
1392
        cs_entry.contents_change = self.make_contents_change(full_path_a,
 
1393
                                                             stat_a, 
 
1394
                                                             full_path_b, 
 
1395
                                                             stat_b)
1522
1396
        return cs_entry
1523
1397
 
1524
 
    def make_exec_flag_change(self, file_id):
 
1398
    def make_exec_flag_change(self, stat_a, stat_b):
1525
1399
        exec_flag_a = exec_flag_b = None
1526
 
        if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
1527
 
            exec_flag_a = self.tree_a.is_executable(file_id)
1528
 
 
1529
 
        if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
1530
 
            exec_flag_b = self.tree_b.is_executable(file_id)
1531
 
 
 
1400
        if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
 
1401
            exec_flag_a = bool(stat_a.st_mode & 0111)
 
1402
        if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
 
1403
            exec_flag_b = bool(stat_b.st_mode & 0111)
1532
1404
        if exec_flag_a == exec_flag_b:
1533
1405
            return None
1534
1406
        return ChangeExecFlag(exec_flag_a, exec_flag_b)
1535
1407
 
1536
 
    def make_contents_change(self, file_id):
1537
 
        a_contents = get_contents(self.tree_a, file_id)
1538
 
        b_contents = get_contents(self.tree_b, file_id)
 
1408
    def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
 
1409
        if stat_a is None and stat_b is None:
 
1410
            return None
 
1411
        if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
 
1412
            stat.S_ISDIR(stat_b.st_mode):
 
1413
            return None
 
1414
        if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
 
1415
            stat.S_ISREG(stat_b.st_mode):
 
1416
            if stat_a.st_ino == stat_b.st_ino and \
 
1417
                stat_a.st_dev == stat_b.st_dev:
 
1418
                return None
 
1419
 
 
1420
        a_contents = self.get_contents(stat_a, full_path_a)
 
1421
        b_contents = self.get_contents(stat_b, full_path_b)
1539
1422
        if a_contents == b_contents:
1540
1423
            return None
1541
1424
        return ReplaceContents(a_contents, b_contents)
1542
1425
 
 
1426
    def get_contents(self, stat_result, full_path):
 
1427
        if stat_result is None:
 
1428
            return None
 
1429
        elif stat.S_ISREG(stat_result.st_mode):
 
1430
            return FileCreate(file(full_path, "rb").read())
 
1431
        elif stat.S_ISDIR(stat_result.st_mode):
 
1432
            return dir_create
 
1433
        elif stat.S_ISLNK(stat_result.st_mode):
 
1434
            return SymlinkCreate(os.readlink(full_path))
 
1435
        else:
 
1436
            raise UnsupportedFiletype(full_path, stat_result)
1543
1437
 
1544
 
def get_contents(tree, file_id):
1545
 
    """Return the appropriate contents to create a copy of file_id from tree"""
1546
 
    if file_id not in tree:
1547
 
        return None
1548
 
    kind = tree.kind(file_id)
1549
 
    if kind == "file":
1550
 
        return TreeFileCreate(tree, file_id)
1551
 
    elif kind in ("directory", "root_directory"):
1552
 
        return dir_create
1553
 
    elif kind == "symlink":
1554
 
        return SymlinkCreate(tree.get_symlink_target(file_id))
1555
 
    else:
1556
 
        raise UnsupportedFiletype(kind, tree.id2path(file_id))
 
1438
    def lstat(self, full_path):
 
1439
        stat_result = None
 
1440
        if full_path is not None:
 
1441
            try:
 
1442
                stat_result = os.lstat(full_path)
 
1443
            except OSError, e:
 
1444
                if e.errno != errno.ENOENT:
 
1445
                    raise
 
1446
        return stat_result
1557
1447
 
1558
1448
 
1559
1449
def full_path(entry, tree):
1560
 
    return os.path.join(tree.basedir, entry.path)
 
1450
    return os.path.join(tree.root, entry.path)
1561
1451
 
1562
1452
def new_delete_entry(entry, tree, inventory, delete):
1563
1453
    if entry.path == "":