~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

[merge] Dennis Duchier, some cleanups of the bzr merge code.

Show diffs side-by-side

added added

removed removed

Lines of Context:
55
55
        self.old_exec_flag = old_exec_flag
56
56
        self.new_exec_flag = new_exec_flag
57
57
 
58
 
    def apply(self, filename, conflict_handler, reverse=False):
59
 
        if not reverse:
60
 
            from_exec_flag = self.old_exec_flag
61
 
            to_exec_flag = self.new_exec_flag
62
 
        else:
63
 
            from_exec_flag = self.new_exec_flag
64
 
            to_exec_flag = self.old_exec_flag
 
58
    def apply(self, filename, conflict_handler):
 
59
        from_exec_flag = self.old_exec_flag
 
60
        to_exec_flag = self.new_exec_flag
65
61
        try:
66
62
            current_exec_flag = bool(os.stat(filename).st_mode & 0111)
67
63
        except OSError, e:
104
100
        return not (self == other)
105
101
 
106
102
 
107
 
def dir_create(filename, conflict_handler, reverse):
 
103
def dir_create(filename, conflict_handler, reverse=False):
108
104
    """Creates the directory, or deletes it if reverse is true.  Intended to be
109
105
    used with ReplaceContents.
110
106
 
149
145
    def __repr__(self):
150
146
        return "SymlinkCreate(%s)" % self.target
151
147
 
152
 
    def __call__(self, filename, conflict_handler, reverse):
 
148
    def __call__(self, filename, conflict_handler, reverse=False):
153
149
        """Creates or destroys the symlink.
154
150
 
155
151
        :param filename: The name of the symlink to create
202
198
    def __ne__(self, other):
203
199
        return not (self == other)
204
200
 
205
 
    def __call__(self, filename, conflict_handler, reverse):
 
201
    def __call__(self, filename, conflict_handler, reverse=False):
206
202
        """Create or delete a file
207
203
 
208
204
        :param filename: The name of the file to create
234
230
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
235
231
                    return
236
232
 
237
 
                    
238
 
 
239
233
class TreeFileCreate(object):
240
234
    """Create or delete a file (for use with ReplaceContents)"""
241
235
    def __init__(self, tree, file_id):
268
262
        in_file = file(filename, "rb")
269
263
        return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
270
264
 
271
 
    def __call__(self, filename, conflict_handler, reverse):
 
265
    def __call__(self, filename, conflict_handler, reverse=False):
272
266
        """Create or delete a file
273
267
 
274
268
        :param filename: The name of the file to create
300
294
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
301
295
                    return
302
296
 
303
 
                    
304
 
 
305
 
def reversed(sequence):
306
 
    max = len(sequence) - 1
307
 
    for i in range(len(sequence)):
308
 
        yield sequence[max - i]
309
 
 
310
297
class ReplaceContents(object):
311
298
    """A contents-replacement framework.  It allows a file/directory/symlink to
312
299
    be created, deleted, or replaced with another file/directory/symlink.
343
330
    def __ne__(self, other):
344
331
        return not (self == other)
345
332
 
346
 
    def apply(self, filename, conflict_handler, reverse=False):
 
333
    def apply(self, filename, conflict_handler):
347
334
        """Applies the FileReplacement to the specified filename
348
335
 
349
336
        :param filename: The name of the file to apply changes to
350
337
        :type filename: str
351
 
        :param reverse: If true, apply the change in reverse
352
 
        :type reverse: bool
353
338
        """
354
 
        if not reverse:
355
 
            undo = self.old_contents
356
 
            perform = self.new_contents
357
 
        else:
358
 
            undo = self.new_contents
359
 
            perform = self.old_contents
 
339
        undo = self.old_contents
 
340
        perform = self.new_contents
360
341
        mode = None
361
342
        if undo is not None:
362
343
            try:
370
351
                    return
371
352
            undo(filename, conflict_handler, reverse=True)
372
353
        if perform is not None:
373
 
            perform(filename, conflict_handler, reverse=False)
 
354
            perform(filename, conflict_handler)
374
355
            if mode is not None:
375
356
                os.chmod(filename, mode)
376
357
 
380
361
    def is_deletion(self):
381
362
        return self.old_contents is not None and self.new_contents is None
382
363
 
383
 
class ApplySequence(object):
384
 
    def __init__(self, changes=None):
385
 
        self.changes = []
386
 
        if changes is not None:
387
 
            self.changes.extend(changes)
388
 
 
389
 
    def __eq__(self, other):
390
 
        if not isinstance(other, ApplySequence):
391
 
            return False
392
 
        elif len(other.changes) != len(self.changes):
393
 
            return False
394
 
        else:
395
 
            for i in range(len(self.changes)):
396
 
                if self.changes[i] != other.changes[i]:
397
 
                    return False
398
 
            return True
399
 
 
400
 
    def __ne__(self, other):
401
 
        return not (self == other)
402
 
 
403
 
    
404
 
    def apply(self, filename, conflict_handler, reverse=False):
405
 
        if not reverse:
406
 
            iter = self.changes
407
 
        else:
408
 
            iter = reversed(self.changes)
409
 
        for change in iter:
410
 
            change.apply(filename, conflict_handler, reverse)
411
 
 
412
364
 
413
365
class Diff3Merge(object):
414
366
    history_based = False
440
392
            out_file.write(line)
441
393
        return out_path
442
394
 
443
 
    def apply(self, filename, conflict_handler, reverse=False):
 
395
    def apply(self, filename, conflict_handler):
444
396
        import bzrlib.patch
445
397
        temp_dir = mkdtemp(prefix="bzr-")
446
398
        try:
447
399
            new_file = filename+".new"
448
400
            base_file = self.dump_file(temp_dir, "base", self.base)
449
401
            other_file = self.dump_file(temp_dir, "other", self.other)
450
 
            if not reverse:
451
 
                base = base_file
452
 
                other = other_file
453
 
            else:
454
 
                base = other_file
455
 
                other = base_file
 
402
            base = base_file
 
403
            other = other_file
456
404
            status = bzrlib.patch.diff3(new_file, filename, base, other)
457
405
            if status == 0:
458
406
                os.chmod(new_file, os.stat(filename).st_mode)
685
633
 
686
634
        return (self.parent != self.new_parent or self.name != self.new_name)
687
635
 
688
 
    def is_deletion(self, reverse):
 
636
    def is_deletion(self, reverse=False):
689
637
        """Return true if applying the entry would delete a file/directory.
690
638
 
691
639
        :param reverse: if true, the changeset is being applied in reverse
693
641
        """
694
642
        return self.is_creation(not reverse)
695
643
 
696
 
    def is_creation(self, reverse):
 
644
    def is_creation(self, reverse=False):
697
645
        """Return true if applying the entry would create a file/directory.
698
646
 
699
647
        :param reverse: if true, the changeset is being applied in reverse
712
660
 
713
661
        :rtype: bool
714
662
        """
715
 
        return self.is_creation(False) or self.is_deletion(False)
 
663
        return self.is_creation() or self.is_deletion()
716
664
 
717
665
    def get_cset_path(self, mod=False):
718
666
        """Determine the path of the entry according to the changeset.
738
686
                return None
739
687
            return self.path
740
688
 
741
 
    def summarize_name(self, reverse=False):
 
689
    def summarize_name(self):
742
690
        """Produce a one-line summary of the filename.  Indicates renames as
743
691
        old => new, indicates creation as None => new, indicates deletion as
744
692
        old => None.
745
693
 
746
 
        :param changeset: The changeset to get paths from
747
 
        :type changeset: `Changeset`
748
 
        :param reverse: If true, reverse the names in the output
749
 
        :type reverse: bool
750
694
        :rtype: str
751
695
        """
752
696
        orig_path = self.get_cset_path(False)
758
702
        if orig_path == mod_path:
759
703
            return orig_path
760
704
        else:
761
 
            if not reverse:
762
 
                return "%s => %s" % (orig_path, mod_path)
763
 
            else:
764
 
                return "%s => %s" % (mod_path, orig_path)
765
 
 
766
 
 
767
 
    def get_new_path(self, id_map, changeset, reverse=False):
 
705
            return "%s => %s" % (orig_path, mod_path)
 
706
 
 
707
 
 
708
    def get_new_path(self, id_map, changeset):
768
709
        """Determine the full pathname to rename to
769
710
 
770
711
        :param id_map: The map of ids to filenames for the tree
771
712
        :type id_map: Dictionary
772
713
        :param changeset: The changeset to get data from
773
714
        :type changeset: `Changeset`
774
 
        :param reverse: If true, we're applying the changeset in reverse
775
 
        :type reverse: bool
776
715
        :rtype: str
777
716
        """
778
717
        mutter("Finding new path for %s", self.summarize_name())
779
 
        if reverse:
780
 
            parent = self.parent
781
 
            to_dir = self.dir
782
 
            from_dir = self.new_dir
783
 
            to_name = self.name
784
 
            from_name = self.new_name
785
 
        else:
786
 
            parent = self.new_parent
787
 
            to_dir = self.new_dir
788
 
            from_dir = self.dir
789
 
            to_name = self.new_name
790
 
            from_name = self.name
 
718
        parent = self.new_parent
 
719
        to_dir = self.new_dir
 
720
        from_dir = self.dir
 
721
        to_name = self.new_name
 
722
        from_name = self.name
791
723
 
792
724
        if to_name is None:
793
725
            return None
802
734
            dir = os.path.dirname(id_map[self.id])
803
735
        else:
804
736
            mutter("path, new_path: %r %r", self.path, self.new_path)
805
 
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
 
737
            dir = parent_entry.get_new_path(id_map, changeset)
806
738
        if from_name == to_name:
807
739
            name = os.path.basename(id_map[self.id])
808
740
        else:
827
759
        else:
828
760
            return True
829
761
 
830
 
    def apply(self, filename, conflict_handler, reverse=False):
 
762
    def apply(self, filename, conflict_handler):
831
763
        """Applies the file content and/or metadata changes.
832
764
 
833
765
        :param filename: the filename of the entry
834
766
        :type filename: str
835
 
        :param reverse: If true, apply the changes in reverse
836
 
        :type reverse: bool
837
767
        """
838
 
        if self.is_deletion(reverse) and self.metadata_change is not None:
839
 
            self.metadata_change.apply(filename, conflict_handler, reverse)
 
768
        if self.is_deletion() and self.metadata_change is not None:
 
769
            self.metadata_change.apply(filename, conflict_handler)
840
770
        if self.contents_change is not None:
841
 
            self.contents_change.apply(filename, conflict_handler, reverse)
842
 
        if not self.is_deletion(reverse) and self.metadata_change is not None:
843
 
            self.metadata_change.apply(filename, conflict_handler, reverse)
 
771
            self.contents_change.apply(filename, conflict_handler)
 
772
        if not self.is_deletion() and self.metadata_change is not None:
 
773
            self.metadata_change.apply(filename, conflict_handler)
844
774
 
845
775
class IDPresent(Exception):
846
776
    def __init__(self, id):
860
790
            raise IDPresent(entry.id)
861
791
        self.entries[entry.id] = entry
862
792
 
863
 
def my_sort(sequence, key, reverse=False):
864
 
    """A sort function that supports supplying a key for comparison
865
 
    
866
 
    :param sequence: The sequence to sort
867
 
    :param key: A callable object that returns the values to be compared
868
 
    :param reverse: If true, sort in reverse order
869
 
    :type reverse: bool
870
 
    """
871
 
    def cmp_by_key(entry_a, entry_b):
872
 
        if reverse:
873
 
            tmp=entry_a
874
 
            entry_a = entry_b
875
 
            entry_b = tmp
876
 
        return cmp(key(entry_a), key(entry_b))
877
 
    sequence.sort(cmp_by_key)
878
 
 
879
 
def get_rename_entries(changeset, inventory, reverse):
 
793
def get_rename_entries(changeset, inventory):
880
794
    """Return a list of entries that will be renamed.  Entries are sorted from
881
795
    longest to shortest source path and from shortest to longest target path.
882
796
 
884
798
    :type changeset: `Changeset`
885
799
    :param inventory: The source of current tree paths for the given ids
886
800
    :type inventory: Dictionary
887
 
    :param reverse: If true, the changeset is being applied in reverse
888
 
    :type reverse: bool
889
801
    :return: source entries and target entries as a tuple
890
802
    :rtype: (List, List)
891
803
    """
899
811
            return 0
900
812
        else:
901
813
            return len(path)
902
 
    my_sort(source_entries, longest_to_shortest, reverse=True)
 
814
    source_entries.sort(None, longest_to_shortest, True)
903
815
 
904
816
    target_entries = source_entries[:]
905
817
    # These are done from shortest to longest path, to avoid creating a
906
818
    # child before its parent has been created/renamed
907
819
    def shortest_to_longest(entry):
908
 
        path = entry.get_new_path(inventory, changeset, reverse)
 
820
        path = entry.get_new_path(inventory, changeset)
909
821
        if path is None:
910
822
            return 0
911
823
        else:
912
824
            return len(path)
913
 
    my_sort(target_entries, shortest_to_longest)
 
825
    target_entries.sort(None, shortest_to_longest)
914
826
    return (source_entries, target_entries)
915
827
 
916
828
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir, 
917
 
                          conflict_handler, reverse):
 
829
                          conflict_handler):
918
830
    """Delete and rename entries as appropriate.  Entries are renamed to temp
919
831
    names.  A map of id -> temp name (or None, for deletions) is returned.
920
832
 
924
836
    :type inventory: Dictionary
925
837
    :param dir: The directory to apply changes to
926
838
    :type dir: str
927
 
    :param reverse: Apply changes in reverse
928
 
    :type reverse: bool
929
839
    :return: a mapping of id to temporary name
930
840
    :rtype: Dictionary
931
841
    """
932
842
    temp_name = {}
933
843
    for i in range(len(source_entries)):
934
844
        entry = source_entries[i]
935
 
        if entry.is_deletion(reverse):
 
845
        if entry.is_deletion():
936
846
            path = pathjoin(dir, inventory[entry.id])
937
 
            entry.apply(path, conflict_handler, reverse)
 
847
            entry.apply(path, conflict_handler)
938
848
            temp_name[entry.id] = None
939
849
 
940
850
        elif entry.needs_rename():
941
 
            if entry.is_creation(reverse):
 
851
            if entry.is_creation():
942
852
                continue
943
853
            to_name = pathjoin(temp_dir, str(i))
944
854
            src_path = inventory.get(entry.id)
958
868
 
959
869
 
960
870
def rename_to_new_create(changed_inventory, target_entries, inventory, 
961
 
                         changeset, dir, conflict_handler, reverse):
 
871
                         changeset, dir, conflict_handler):
962
872
    """Rename entries with temp names to their final names, create new files.
963
873
 
964
874
    :param changed_inventory: A mapping of id to temporary name
969
879
    :type changeset: `Changeset`
970
880
    :param dir: The directory to apply changes to
971
881
    :type dir: str
972
 
    :param reverse: If true, apply changes in reverse
973
 
    :type reverse: bool
974
882
    """
975
883
    for entry in target_entries:
976
 
        new_tree_path = entry.get_new_path(inventory, changeset, reverse)
 
884
        new_tree_path = entry.get_new_path(inventory, changeset)
977
885
        if new_tree_path is None:
978
886
            continue
979
887
        new_path = pathjoin(dir, new_tree_path)
982
890
            if conflict_handler.target_exists(entry, new_path, old_path) == \
983
891
                "skip":
984
892
                continue
985
 
        if entry.is_creation(reverse):
986
 
            entry.apply(new_path, conflict_handler, reverse)
 
893
        if entry.is_creation():
 
894
            entry.apply(new_path, conflict_handler)
987
895
            changed_inventory[entry.id] = new_tree_path
988
896
        elif entry.needs_rename():
989
 
            if entry.is_deletion(reverse):
 
897
            if entry.is_deletion():
990
898
                continue
991
899
            if old_path is None:
992
900
                continue
1183
1091
    def finalize(self):
1184
1092
        pass
1185
1093
 
1186
 
def apply_changeset(changeset, inventory, dir, conflict_handler=None, 
1187
 
                    reverse=False):
 
1094
def apply_changeset(changeset, inventory, dir, conflict_handler=None):
1188
1095
    """Apply a changeset to a directory.
1189
1096
 
1190
1097
    :param changeset: The changes to perform
1193
1100
    :type inventory: Dictionary
1194
1101
    :param dir: The path of the directory to apply the changes to
1195
1102
    :type dir: str
1196
 
    :param reverse: If true, apply the changes in reverse
1197
 
    :type reverse: bool
1198
1103
    :return: The mapping of the changed entries
1199
1104
    :rtype: Dictionary
1200
1105
    """
1222
1127
                        entry.id)
1223
1128
                continue
1224
1129
            path = pathjoin(dir, inventory[entry.id])
1225
 
            entry.apply(path, conflict_handler, reverse)
 
1130
            entry.apply(path, conflict_handler)
1226
1131
 
1227
1132
    # Apply renames in stages, to minimize conflicts:
1228
1133
    # Only files whose name or parent change are interesting, because their
1229
1134
    # target name may exist in the source tree.  If a directory's name changes,
1230
1135
    # that doesn't make its children interesting.
1231
 
    (source_entries, target_entries) = get_rename_entries(changeset, inventory,
1232
 
                                                          reverse)
 
1136
    (source_entries, target_entries) = get_rename_entries(changeset, inventory)
1233
1137
 
1234
1138
    changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1235
 
                                              temp_dir, conflict_handler,
1236
 
                                              reverse)
 
1139
                                              temp_dir, conflict_handler)
1237
1140
 
1238
1141
    rename_to_new_create(changed_inventory, target_entries, inventory,
1239
 
                         changeset, dir, conflict_handler, reverse)
 
1142
                         changeset, dir, conflict_handler)
1240
1143
    os.rmdir(temp_dir)
1241
1144
    return changed_inventory
1242
1145
 
1243
1146
 
1244
 
def apply_changeset_tree(cset, tree, reverse=False):
 
1147
def apply_changeset_tree(cset, tree):
1245
1148
    r_inventory = {}
1246
1149
    for entry in tree.source_inventory().itervalues():
1247
1150
        inventory[entry.id] = entry.path
1248
 
    new_inventory = apply_changeset(cset, r_inventory, tree.basedir,
1249
 
                                    reverse=reverse)
 
1151
    new_inventory = apply_changeset(cset, r_inventory, tree.basedir)
1250
1152
    new_entries, remove_entries = \
1251
 
        get_inventory_change(inventory, new_inventory, cset, reverse)
 
1153
        get_inventory_change(inventory, new_inventory, cset)
1252
1154
    tree.update_source_inventory(new_entries, remove_entries)
1253
1155
 
1254
1156
 
1255
 
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
 
1157
def get_inventory_change(inventory, new_inventory, cset):
1256
1158
    new_entries = {}
1257
1159
    remove_entries = []
1258
1160
    for entry in cset.entries.itervalues():
1277
1179
        print entry.id
1278
1180
        print entry.summarize_name(cset)
1279
1181
 
1280
 
class CompositionFailure(Exception):
1281
 
    def __init__(self, old_entry, new_entry, problem):
1282
 
        msg = "Unable to conpose entries.\n %s" % problem
1283
 
        Exception.__init__(self, msg)
1284
 
 
1285
 
class IDMismatch(CompositionFailure):
1286
 
    def __init__(self, old_entry, new_entry):
1287
 
        problem = "Attempt to compose entries with different ids: %s and %s" %\
1288
 
            (old_entry.id, new_entry.id)
1289
 
        CompositionFailure.__init__(self, old_entry, new_entry, problem)
1290
 
 
1291
 
def compose_changesets(old_cset, new_cset):
1292
 
    """Combine two changesets into one.  This works well for exact patching.
1293
 
    Otherwise, not so well.
1294
 
 
1295
 
    :param old_cset: The first changeset that would be applied
1296
 
    :type old_cset: `Changeset`
1297
 
    :param new_cset: The second changeset that would be applied
1298
 
    :type new_cset: `Changeset`
1299
 
    :return: A changeset that combines the changes in both changesets
1300
 
    :rtype: `Changeset`
1301
 
    """
1302
 
    composed = Changeset()
1303
 
    for old_entry in old_cset.entries.itervalues():
1304
 
        new_entry = new_cset.entries.get(old_entry.id)
1305
 
        if new_entry is None:
1306
 
            composed.add_entry(old_entry)
1307
 
        else:
1308
 
            composed_entry = compose_entries(old_entry, new_entry)
1309
 
            if composed_entry.parent is not None or\
1310
 
                composed_entry.new_parent is not None:
1311
 
                composed.add_entry(composed_entry)
1312
 
    for new_entry in new_cset.entries.itervalues():
1313
 
        if not old_cset.entries.has_key(new_entry.id):
1314
 
            composed.add_entry(new_entry)
1315
 
    return composed
1316
 
 
1317
 
def compose_entries(old_entry, new_entry):
1318
 
    """Combine two entries into one.
1319
 
 
1320
 
    :param old_entry: The first entry that would be applied
1321
 
    :type old_entry: ChangesetEntry
1322
 
    :param old_entry: The second entry that would be applied
1323
 
    :type old_entry: ChangesetEntry
1324
 
    :return: A changeset entry combining both entries
1325
 
    :rtype: `ChangesetEntry`
1326
 
    """
1327
 
    if old_entry.id != new_entry.id:
1328
 
        raise IDMismatch(old_entry, new_entry)
1329
 
    output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1330
 
 
1331
 
    if (old_entry.parent != old_entry.new_parent or 
1332
 
        new_entry.parent != new_entry.new_parent):
1333
 
        output.new_parent = new_entry.new_parent
1334
 
 
1335
 
    if (old_entry.path != old_entry.new_path or 
1336
 
        new_entry.path != new_entry.new_path):
1337
 
        output.new_path = new_entry.new_path
1338
 
 
1339
 
    output.contents_change = compose_contents(old_entry, new_entry)
1340
 
    output.metadata_change = compose_metadata(old_entry, new_entry)
1341
 
    return output
1342
 
 
1343
 
def compose_contents(old_entry, new_entry):
1344
 
    """Combine the contents of two changeset entries.  Entries are combined
1345
 
    intelligently where possible, but the fallback behavior returns an 
1346
 
    ApplySequence.
1347
 
 
1348
 
    :param old_entry: The first entry that would be applied
1349
 
    :type old_entry: `ChangesetEntry`
1350
 
    :param new_entry: The second entry that would be applied
1351
 
    :type new_entry: `ChangesetEntry`
1352
 
    :return: A combined contents change
1353
 
    :rtype: anything supporting the apply(reverse=False) method
1354
 
    """
1355
 
    old_contents = old_entry.contents_change
1356
 
    new_contents = new_entry.contents_change
1357
 
    if old_entry.contents_change is None:
1358
 
        return new_entry.contents_change
1359
 
    elif new_entry.contents_change is None:
1360
 
        return old_entry.contents_change
1361
 
    elif isinstance(old_contents, ReplaceContents) and \
1362
 
        isinstance(new_contents, ReplaceContents):
1363
 
        if old_contents.old_contents == new_contents.new_contents:
1364
 
            return None
1365
 
        else:
1366
 
            return ReplaceContents(old_contents.old_contents,
1367
 
                                   new_contents.new_contents)
1368
 
    elif isinstance(old_contents, ApplySequence):
1369
 
        output = ApplySequence(old_contents.changes)
1370
 
        if isinstance(new_contents, ApplySequence):
1371
 
            output.changes.extend(new_contents.changes)
1372
 
        else:
1373
 
            output.changes.append(new_contents)
1374
 
        return output
1375
 
    elif isinstance(new_contents, ApplySequence):
1376
 
        output = ApplySequence((old_contents.changes,))
1377
 
        output.extend(new_contents.changes)
1378
 
        return output
1379
 
    else:
1380
 
        return ApplySequence((old_contents, new_contents))
1381
 
 
1382
 
def compose_metadata(old_entry, new_entry):
1383
 
    old_meta = old_entry.metadata_change
1384
 
    new_meta = new_entry.metadata_change
1385
 
    if old_meta is None:
1386
 
        return new_meta
1387
 
    elif new_meta is None:
1388
 
        return old_meta
1389
 
    elif (isinstance(old_meta, ChangeExecFlag) and
1390
 
          isinstance(new_meta, ChangeExecFlag)):
1391
 
        return ChangeExecFlag(old_meta.old_exec_flag, new_meta.new_exec_flag)
1392
 
    else:
1393
 
        return ApplySequence(old_meta, new_meta)
1394
 
 
1395
 
 
1396
 
def changeset_is_null(changeset):
1397
 
    for entry in changeset.entries.itervalues():
1398
 
        if not entry.is_boring():
1399
 
            return False
1400
 
    return True
1401
 
 
1402
1182
class UnsupportedFiletype(Exception):
1403
1183
    def __init__(self, kind, full_path):
1404
1184
        msg = "The file \"%s\" is a %s, which is not a supported filetype." \