~bzr-pqm/bzr/bzr.dev

493 by Martin Pool
- Merge aaron's merge command
1
# Copyright (C) 2004 Aaron Bentley <aaron.bentley@utoronto.ca>
2
#
3
#    This program is free software; you can redistribute it and/or modify
4
#    it under the terms of the GNU General Public License as published by
5
#    the Free Software Foundation; either version 2 of the License, or
6
#    (at your option) any later version.
7
#
8
#    This program is distributed in the hope that it will be useful,
9
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
#    GNU General Public License for more details.
12
#
13
#    You should have received a copy of the GNU General Public License
14
#    along with this program; if not, write to the Free Software
15
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
import os.path
17
import errno
18
import patch
19
import stat
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
20
from bzrlib.trace import mutter
1185.1.40 by Robert Collins
Merge what applied of Alexander Belchenko's win32 patch.
21
from bzrlib.osutils import rename
974.1.26 by aaron.bentley at utoronto
merged mbp@sourcefrog.net-20050817233101-0939da1cf91f2472
22
23
# XXX: mbp: I'm not totally convinced that we should handle conflicts
24
# as part of changeset application, rather than only in the merge
25
# operation.
26
27
"""Represent and apply a changeset
28
29
Conflicts in applying a changeset are represented as exceptions.
30
"""
31
493 by Martin Pool
- Merge aaron's merge command
32
__docformat__ = "restructuredtext"
33
34
NULL_ID = "!NULL"
35
850 by Martin Pool
- Merge merge updates from aaron
36
class OldFailedTreeOp(Exception):
37
    def __init__(self):
38
        Exception.__init__(self, "bzr-tree-change contains files from a"
39
                           " previous failed merge operation.")
493 by Martin Pool
- Merge aaron's merge command
40
def invert_dict(dict):
41
    newdict = {}
42
    for (key,value) in dict.iteritems():
43
        newdict[value] = key
44
    return newdict
45
974.1.15 by aaron.bentley at utoronto
Removed use of patch and diff in merge, removed patch.diff
46
       
558 by Martin Pool
- All top-level classes inherit from object
47
class ChangeUnixPermissions(object):
493 by Martin Pool
- Merge aaron's merge command
48
    """This is two-way change, suitable for file modification, creation,
49
    deletion"""
50
    def __init__(self, old_mode, new_mode):
51
        self.old_mode = old_mode
52
        self.new_mode = new_mode
53
54
    def apply(self, filename, conflict_handler, reverse=False):
55
        if not reverse:
56
            from_mode = self.old_mode
57
            to_mode = self.new_mode
58
        else:
59
            from_mode = self.new_mode
60
            to_mode = self.old_mode
61
        try:
62
            current_mode = os.stat(filename).st_mode &0777
63
        except OSError, e:
64
            if e.errno == errno.ENOENT:
65
                if conflict_handler.missing_for_chmod(filename) == "skip":
66
                    return
67
                else:
68
                    current_mode = from_mode
69
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":
73
                return
74
75
        if to_mode is not None:
76
            try:
77
                os.chmod(filename, to_mode)
78
            except IOError, e:
79
                if e.errno == errno.ENOENT:
80
                    conflict_handler.missing_for_chmod(filename)
81
82
    def __eq__(self, other):
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
91
92
    def __ne__(self, other):
93
        return not (self == other)
94
1185.1.41 by Robert Collins
massive patch from Alexander Belchenko - many PEP8 fixes, removes unused function uuid
95
493 by Martin Pool
- Merge aaron's merge command
96
def dir_create(filename, conflict_handler, reverse):
97
    """Creates the directory, or deletes it if reverse is true.  Intended to be
98
    used with ReplaceContents.
99
100
    :param filename: The name of the directory to create
101
    :type filename: str
102
    :param reverse: If true, delete the directory, instead
103
    :type reverse: bool
104
    """
105
    if not reverse:
106
        try:
107
            os.mkdir(filename)
108
        except OSError, e:
109
            if e.errno != errno.EEXIST:
110
                raise
111
            if conflict_handler.dir_exists(filename) == "continue":
112
                os.mkdir(filename)
113
        except IOError, e:
114
            if e.errno == errno.ENOENT:
115
                if conflict_handler.missing_parent(filename)=="continue":
116
                    file(filename, "wb").write(self.contents)
117
    else:
118
        try:
119
            os.rmdir(filename)
120
        except OSError, e:
1185.1.41 by Robert Collins
massive patch from Alexander Belchenko - many PEP8 fixes, removes unused function uuid
121
            if e.errno != errno.ENOTEMPTY:
493 by Martin Pool
- Merge aaron's merge command
122
                raise
123
            if conflict_handler.rmdir_non_empty(filename) == "skip":
124
                return
125
            os.rmdir(filename)
126
127
558 by Martin Pool
- All top-level classes inherit from object
128
class SymlinkCreate(object):
493 by Martin Pool
- Merge aaron's merge command
129
    """Creates or deletes a symlink (for use with ReplaceContents)"""
130
    def __init__(self, contents):
131
        """Constructor.
132
133
        :param contents: The filename of the target the symlink should point to
134
        :type contents: str
135
        """
136
        self.target = contents
137
138
    def __call__(self, filename, conflict_handler, reverse):
139
        """Creates or destroys the symlink.
140
141
        :param filename: The name of the symlink to create
142
        :type filename: str
143
        """
144
        if reverse:
145
            assert(os.readlink(filename) == self.target)
146
            os.unlink(filename)
147
        else:
148
            try:
149
                os.symlink(self.target, filename)
150
            except OSError, e:
151
                if e.errno != errno.EEXIST:
152
                    raise
153
                if conflict_handler.link_name_exists(filename) == "continue":
154
                    os.symlink(self.target, filename)
155
156
    def __eq__(self, other):
157
        if not isinstance(other, SymlinkCreate):
158
            return False
159
        elif self.target != other.target:
160
            return False
161
        else:
162
            return True
163
164
    def __ne__(self, other):
165
        return not (self == other)
166
558 by Martin Pool
- All top-level classes inherit from object
167
class FileCreate(object):
493 by Martin Pool
- Merge aaron's merge command
168
    """Create or delete a file (for use with ReplaceContents)"""
169
    def __init__(self, contents):
170
        """Constructor
171
172
        :param contents: The contents of the file to write
173
        :type contents: str
174
        """
175
        self.contents = contents
176
177
    def __repr__(self):
178
        return "FileCreate(%i b)" % len(self.contents)
179
180
    def __eq__(self, other):
181
        if not isinstance(other, FileCreate):
182
            return False
183
        elif self.contents != other.contents:
184
            return False
185
        else:
186
            return True
187
188
    def __ne__(self, other):
189
        return not (self == other)
190
191
    def __call__(self, filename, conflict_handler, reverse):
192
        """Create or delete a file
193
194
        :param filename: The name of the file to create
195
        :type filename: str
196
        :param reverse: Delete the file instead of creating it
197
        :type reverse: bool
198
        """
199
        if not reverse:
200
            try:
201
                file(filename, "wb").write(self.contents)
202
            except IOError, e:
203
                if e.errno == errno.ENOENT:
204
                    if conflict_handler.missing_parent(filename)=="continue":
205
                        file(filename, "wb").write(self.contents)
206
                else:
207
                    raise
208
209
        else:
210
            try:
211
                if (file(filename, "rb").read() != self.contents):
212
                    direction = conflict_handler.wrong_old_contents(filename,
213
                                                                    self.contents)
214
                    if  direction != "continue":
215
                        return
216
                os.unlink(filename)
217
            except IOError, e:
218
                if e.errno != errno.ENOENT:
219
                    raise
220
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
221
                    return
222
223
                    
224
225
def reversed(sequence):
226
    max = len(sequence) - 1
227
    for i in range(len(sequence)):
228
        yield sequence[max - i]
229
558 by Martin Pool
- All top-level classes inherit from object
230
class ReplaceContents(object):
493 by Martin Pool
- Merge aaron's merge command
231
    """A contents-replacement framework.  It allows a file/directory/symlink to
232
    be created, deleted, or replaced with another file/directory/symlink.
233
    Arguments must be callable with (filename, reverse).
234
    """
235
    def __init__(self, old_contents, new_contents):
236
        """Constructor.
237
238
        :param old_contents: The change to reverse apply (e.g. a deletion), \
239
        when going forwards.
240
        :type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
241
        NoneType, etc.
242
        :param new_contents: The second change to apply (e.g. a creation), \
243
        when going forwards.
244
        :type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
245
        NoneType, etc.
246
        """
247
        self.old_contents=old_contents
248
        self.new_contents=new_contents
249
250
    def __repr__(self):
251
        return "ReplaceContents(%r -> %r)" % (self.old_contents,
252
                                              self.new_contents)
253
254
    def __eq__(self, other):
255
        if not isinstance(other, ReplaceContents):
256
            return False
257
        elif self.old_contents != other.old_contents:
258
            return False
259
        elif self.new_contents != other.new_contents:
260
            return False
261
        else:
262
            return True
263
    def __ne__(self, other):
264
        return not (self == other)
265
266
    def apply(self, filename, conflict_handler, reverse=False):
267
        """Applies the FileReplacement to the specified filename
268
269
        :param filename: The name of the file to apply changes to
270
        :type filename: str
271
        :param reverse: If true, apply the change in reverse
272
        :type reverse: bool
273
        """
274
        if not reverse:
275
            undo = self.old_contents
276
            perform = self.new_contents
277
        else:
278
            undo = self.new_contents
279
            perform = self.old_contents
280
        mode = None
281
        if undo is not None:
282
            try:
283
                mode = os.lstat(filename).st_mode
284
                if stat.S_ISLNK(mode):
285
                    mode = None
286
            except OSError, e:
287
                if e.errno != errno.ENOENT:
288
                    raise
289
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
290
                    return
291
            undo(filename, conflict_handler, reverse=True)
292
        if perform is not None:
293
            perform(filename, conflict_handler, reverse=False)
294
            if mode is not None:
295
                os.chmod(filename, mode)
296
558 by Martin Pool
- All top-level classes inherit from object
297
class ApplySequence(object):
493 by Martin Pool
- Merge aaron's merge command
298
    def __init__(self, changes=None):
299
        self.changes = []
300
        if changes is not None:
301
            self.changes.extend(changes)
302
303
    def __eq__(self, other):
304
        if not isinstance(other, ApplySequence):
305
            return False
306
        elif len(other.changes) != len(self.changes):
307
            return False
308
        else:
309
            for i in range(len(self.changes)):
310
                if self.changes[i] != other.changes[i]:
311
                    return False
312
            return True
313
314
    def __ne__(self, other):
315
        return not (self == other)
316
317
    
318
    def apply(self, filename, conflict_handler, reverse=False):
319
        if not reverse:
320
            iter = self.changes
321
        else:
322
            iter = reversed(self.changes)
323
        for change in iter:
324
            change.apply(filename, conflict_handler, reverse)
325
326
558 by Martin Pool
- All top-level classes inherit from object
327
class Diff3Merge(object):
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
328
    def __init__(self, file_id, base, other):
329
        self.file_id = file_id
330
        self.base = base
331
        self.other = other
493 by Martin Pool
- Merge aaron's merge command
332
333
    def __eq__(self, other):
334
        if not isinstance(other, Diff3Merge):
335
            return False
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
336
        return (self.base == other.base and 
337
                self.other == other.other and self.file_id == other.file_id)
493 by Martin Pool
- Merge aaron's merge command
338
339
    def __ne__(self, other):
340
        return not (self == other)
341
342
    def apply(self, filename, conflict_handler, reverse=False):
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
343
        new_file = filename+".new"
344
        base_file = self.base.readonly_path(self.file_id)
345
        other_file = self.other.readonly_path(self.file_id)
493 by Martin Pool
- Merge aaron's merge command
346
        if not reverse:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
347
            base = base_file
348
            other = other_file
493 by Martin Pool
- Merge aaron's merge command
349
        else:
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
350
            base = other_file
351
            other = base_file
493 by Martin Pool
- Merge aaron's merge command
352
        status = patch.diff3(new_file, filename, base, other)
353
        if status == 0:
354
            os.chmod(new_file, os.stat(filename).st_mode)
1185.1.40 by Robert Collins
Merge what applied of Alexander Belchenko's win32 patch.
355
            rename(new_file, filename)
493 by Martin Pool
- Merge aaron's merge command
356
            return
357
        else:
358
            assert(status == 1)
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
359
            def get_lines(filename):
360
                my_file = file(base, "rb")
361
                lines = my_file.readlines()
362
                my_file.close()
363
            base_lines = get_lines(base)
364
            other_lines = get_lines(other)
365
            conflict_handler.merge_conflict(new_file, filename, base_lines, 
366
                                            other_lines)
493 by Martin Pool
- Merge aaron's merge command
367
368
369
def CreateDir():
370
    """Convenience function to create a directory.
371
372
    :return: A ReplaceContents that will create a directory
373
    :rtype: `ReplaceContents`
374
    """
375
    return ReplaceContents(None, dir_create)
376
377
def DeleteDir():
378
    """Convenience function to delete a directory.
379
380
    :return: A ReplaceContents that will delete a directory
381
    :rtype: `ReplaceContents`
382
    """
383
    return ReplaceContents(dir_create, None)
384
385
def CreateFile(contents):
386
    """Convenience fucntion to create a file.
387
    
388
    :param contents: The contents of the file to create 
389
    :type contents: str
390
    :return: A ReplaceContents that will create a file 
391
    :rtype: `ReplaceContents`
392
    """
393
    return ReplaceContents(None, FileCreate(contents))
394
395
def DeleteFile(contents):
396
    """Convenience fucntion to delete a file.
397
    
398
    :param contents: The contents of the file to delete
399
    :type contents: str
400
    :return: A ReplaceContents that will delete a file 
401
    :rtype: `ReplaceContents`
402
    """
403
    return ReplaceContents(FileCreate(contents), None)
404
405
def ReplaceFileContents(old_contents, new_contents):
406
    """Convenience fucntion to replace the contents of a file.
407
    
408
    :param old_contents: The contents of the file to replace 
409
    :type old_contents: str
410
    :param new_contents: The contents to replace the file with
411
    :type new_contents: str
412
    :return: A ReplaceContents that will replace the contents of a file a file 
413
    :rtype: `ReplaceContents`
414
    """
415
    return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
416
417
def CreateSymlink(target):
418
    """Convenience fucntion to create a symlink.
419
    
420
    :param target: The path the link should point to
421
    :type target: str
422
    :return: A ReplaceContents that will delete a file 
423
    :rtype: `ReplaceContents`
424
    """
425
    return ReplaceContents(None, SymlinkCreate(target))
426
427
def DeleteSymlink(target):
428
    """Convenience fucntion to delete a symlink.
429
    
430
    :param target: The path the link should point to
431
    :type target: str
432
    :return: A ReplaceContents that will delete a file 
433
    :rtype: `ReplaceContents`
434
    """
435
    return ReplaceContents(SymlinkCreate(target), None)
436
437
def ChangeTarget(old_target, new_target):
438
    """Convenience fucntion to change the target of a symlink.
439
    
440
    :param old_target: The current link target
441
    :type old_target: str
442
    :param new_target: The new link target to use
443
    :type new_target: str
444
    :return: A ReplaceContents that will delete a file 
445
    :rtype: `ReplaceContents`
446
    """
447
    return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
448
449
450
class InvalidEntry(Exception):
451
    """Raise when a ChangesetEntry is invalid in some way"""
452
    def __init__(self, entry, problem):
453
        """Constructor.
454
455
        :param entry: The invalid ChangesetEntry
456
        :type entry: `ChangesetEntry`
457
        :param problem: The problem with the entry
458
        :type problem: str
459
        """
460
        msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id, 
461
                                                               entry.path, 
462
                                                               problem)
463
        Exception.__init__(self, msg)
464
        self.entry = entry
465
466
467
class SourceRootHasName(InvalidEntry):
468
    """This changeset entry has a name other than "", but its parent is !NULL"""
469
    def __init__(self, entry, name):
470
        """Constructor.
471
472
        :param entry: The invalid ChangesetEntry
473
        :type entry: `ChangesetEntry`
474
        :param name: The name of the entry
475
        :type name: str
476
        """
477
        msg = 'Child of !NULL is named "%s", not "./.".' % name
478
        InvalidEntry.__init__(self, entry, msg)
479
480
class NullIDAssigned(InvalidEntry):
481
    """The id !NULL was assigned to a real entry"""
482
    def __init__(self, entry):
483
        """Constructor.
484
485
        :param entry: The invalid ChangesetEntry
486
        :type entry: `ChangesetEntry`
487
        """
488
        msg = '"!NULL" id assigned to a file "%s".' % entry.path
489
        InvalidEntry.__init__(self, entry, msg)
490
491
class ParentIDIsSelf(InvalidEntry):
492
    """An entry is marked as its own parent"""
493
    def __init__(self, entry):
494
        """Constructor.
495
496
        :param entry: The invalid ChangesetEntry
497
        :type entry: `ChangesetEntry`
498
        """
499
        msg = 'file %s has "%s" id for both self id and parent id.' % \
500
            (entry.path, entry.id)
501
        InvalidEntry.__init__(self, entry, msg)
502
503
class ChangesetEntry(object):
504
    """An entry the changeset"""
505
    def __init__(self, id, parent, path):
506
        """Constructor. Sets parent and name assuming it was not
507
        renamed/created/deleted.
508
        :param id: The id associated with the entry
509
        :param parent: The id of the parent of this entry (or !NULL if no
510
        parent)
511
        :param path: The file path relative to the tree root of this entry
512
        """
513
        self.id = id
514
        self.path = path 
515
        self.new_path = path
516
        self.parent = parent
517
        self.new_parent = parent
518
        self.contents_change = None
519
        self.metadata_change = None
520
        if parent == NULL_ID and path !='./.':
521
            raise SourceRootHasName(self, path)
522
        if self.id == NULL_ID:
523
            raise NullIDAssigned(self)
524
        if self.id  == self.parent:
525
            raise ParentIDIsSelf(self)
526
527
    def __str__(self):
528
        return "ChangesetEntry(%s)" % self.id
529
530
    def __get_dir(self):
531
        if self.path is None:
532
            return None
533
        return os.path.dirname(self.path)
534
535
    def __set_dir(self, dir):
536
        self.path = os.path.join(dir, os.path.basename(self.path))
537
538
    dir = property(__get_dir, __set_dir)
539
    
540
    def __get_name(self):
541
        if self.path is None:
542
            return None
543
        return os.path.basename(self.path)
544
545
    def __set_name(self, name):
546
        self.path = os.path.join(os.path.dirname(self.path), name)
547
548
    name = property(__get_name, __set_name)
549
550
    def __get_new_dir(self):
551
        if self.new_path is None:
552
            return None
553
        return os.path.dirname(self.new_path)
554
555
    def __set_new_dir(self, dir):
556
        self.new_path = os.path.join(dir, os.path.basename(self.new_path))
557
558
    new_dir = property(__get_new_dir, __set_new_dir)
559
560
    def __get_new_name(self):
561
        if self.new_path is None:
562
            return None
563
        return os.path.basename(self.new_path)
564
565
    def __set_new_name(self, name):
566
        self.new_path = os.path.join(os.path.dirname(self.new_path), name)
567
568
    new_name = property(__get_new_name, __set_new_name)
569
570
    def needs_rename(self):
571
        """Determines whether the entry requires renaming.
572
573
        :rtype: bool
574
        """
575
576
        return (self.parent != self.new_parent or self.name != self.new_name)
577
578
    def is_deletion(self, reverse):
579
        """Return true if applying the entry would delete a file/directory.
580
581
        :param reverse: if true, the changeset is being applied in reverse
582
        :rtype: bool
583
        """
584
        return ((self.new_parent is None and not reverse) or 
585
                (self.parent is None and reverse))
586
587
    def is_creation(self, reverse):
588
        """Return true if applying the entry would create a file/directory.
589
590
        :param reverse: if true, the changeset is being applied in reverse
591
        :rtype: bool
592
        """
593
        return ((self.parent is None and not reverse) or 
594
                (self.new_parent is None and reverse))
595
596
    def is_creation_or_deletion(self):
597
        """Return true if applying the entry would create or delete a 
598
        file/directory.
599
600
        :rtype: bool
601
        """
602
        return self.parent is None or self.new_parent is None
603
604
    def get_cset_path(self, mod=False):
605
        """Determine the path of the entry according to the changeset.
606
607
        :param changeset: The changeset to derive the path from
608
        :type changeset: `Changeset`
609
        :param mod: If true, generate the MOD path.  Otherwise, generate the \
610
        ORIG path.
611
        :return: the path of the entry, or None if it did not exist in the \
612
        requested tree.
613
        :rtype: str or NoneType
614
        """
615
        if mod:
616
            if self.new_parent == NULL_ID:
617
                return "./."
618
            elif self.new_parent is None:
619
                return None
620
            return self.new_path
621
        else:
622
            if self.parent == NULL_ID:
623
                return "./."
624
            elif self.parent is None:
625
                return None
626
            return self.path
627
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
628
    def summarize_name(self, reverse=False):
493 by Martin Pool
- Merge aaron's merge command
629
        """Produce a one-line summary of the filename.  Indicates renames as
630
        old => new, indicates creation as None => new, indicates deletion as
631
        old => None.
632
633
        :param changeset: The changeset to get paths from
634
        :type changeset: `Changeset`
635
        :param reverse: If true, reverse the names in the output
636
        :type reverse: bool
637
        :rtype: str
638
        """
639
        orig_path = self.get_cset_path(False)
640
        mod_path = self.get_cset_path(True)
641
        if orig_path is not None:
642
            orig_path = orig_path[2:]
643
        if mod_path is not None:
644
            mod_path = mod_path[2:]
645
        if orig_path == mod_path:
646
            return orig_path
647
        else:
648
            if not reverse:
649
                return "%s => %s" % (orig_path, mod_path)
650
            else:
651
                return "%s => %s" % (mod_path, orig_path)
652
653
654
    def get_new_path(self, id_map, changeset, reverse=False):
655
        """Determine the full pathname to rename to
656
657
        :param id_map: The map of ids to filenames for the tree
658
        :type id_map: Dictionary
659
        :param changeset: The changeset to get data from
660
        :type changeset: `Changeset`
661
        :param reverse: If true, we're applying the changeset in reverse
662
        :type reverse: bool
663
        :rtype: str
664
        """
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
665
        mutter("Finding new path for %s" % self.summarize_name())
493 by Martin Pool
- Merge aaron's merge command
666
        if reverse:
667
            parent = self.parent
668
            to_dir = self.dir
669
            from_dir = self.new_dir
670
            to_name = self.name
671
            from_name = self.new_name
672
        else:
673
            parent = self.new_parent
674
            to_dir = self.new_dir
675
            from_dir = self.dir
676
            to_name = self.new_name
677
            from_name = self.name
678
679
        if to_name is None:
680
            return None
681
682
        if parent == NULL_ID or parent is None:
683
            if to_name != '.':
684
                raise SourceRootHasName(self, to_name)
685
            else:
686
                return '.'
687
        if from_dir == to_dir:
688
            dir = os.path.dirname(id_map[self.id])
689
        else:
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
690
            mutter("path, new_path: %r %r" % (self.path, self.new_path))
493 by Martin Pool
- Merge aaron's merge command
691
            parent_entry = changeset.entries[parent]
692
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
693
        if from_name == to_name:
694
            name = os.path.basename(id_map[self.id])
695
        else:
696
            name = to_name
697
            assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
698
        return os.path.join(dir, name)
699
700
    def is_boring(self):
701
        """Determines whether the entry does nothing
702
        
703
        :return: True if the entry does no renames or content changes
704
        :rtype: bool
705
        """
706
        if self.contents_change is not None:
707
            return False
708
        elif self.metadata_change is not None:
709
            return False
710
        elif self.parent != self.new_parent:
711
            return False
712
        elif self.name != self.new_name:
713
            return False
714
        else:
715
            return True
716
717
    def apply(self, filename, conflict_handler, reverse=False):
718
        """Applies the file content and/or metadata changes.
719
720
        :param filename: the filename of the entry
721
        :type filename: str
722
        :param reverse: If true, apply the changes in reverse
723
        :type reverse: bool
724
        """
725
        if self.is_deletion(reverse) and self.metadata_change is not None:
726
            self.metadata_change.apply(filename, conflict_handler, reverse)
727
        if self.contents_change is not None:
728
            self.contents_change.apply(filename, conflict_handler, reverse)
729
        if not self.is_deletion(reverse) and self.metadata_change is not None:
730
            self.metadata_change.apply(filename, conflict_handler, reverse)
731
732
class IDPresent(Exception):
733
    def __init__(self, id):
734
        msg = "Cannot add entry because that id has already been used:\n%s" %\
735
            id
736
        Exception.__init__(self, msg)
737
        self.id = id
738
558 by Martin Pool
- All top-level classes inherit from object
739
class Changeset(object):
493 by Martin Pool
- Merge aaron's merge command
740
    """A set of changes to apply"""
741
    def __init__(self):
742
        self.entries = {}
743
744
    def add_entry(self, entry):
745
        """Add an entry to the list of entries"""
746
        if self.entries.has_key(entry.id):
747
            raise IDPresent(entry.id)
748
        self.entries[entry.id] = entry
749
750
def my_sort(sequence, key, reverse=False):
751
    """A sort function that supports supplying a key for comparison
752
    
753
    :param sequence: The sequence to sort
754
    :param key: A callable object that returns the values to be compared
755
    :param reverse: If true, sort in reverse order
756
    :type reverse: bool
757
    """
758
    def cmp_by_key(entry_a, entry_b):
759
        if reverse:
760
            tmp=entry_a
761
            entry_a = entry_b
762
            entry_b = tmp
763
        return cmp(key(entry_a), key(entry_b))
764
    sequence.sort(cmp_by_key)
765
766
def get_rename_entries(changeset, inventory, reverse):
767
    """Return a list of entries that will be renamed.  Entries are sorted from
768
    longest to shortest source path and from shortest to longest target path.
769
770
    :param changeset: The changeset to look in
771
    :type changeset: `Changeset`
772
    :param inventory: The source of current tree paths for the given ids
773
    :type inventory: Dictionary
774
    :param reverse: If true, the changeset is being applied in reverse
775
    :type reverse: bool
776
    :return: source entries and target entries as a tuple
777
    :rtype: (List, List)
778
    """
779
    source_entries = [x for x in changeset.entries.itervalues() 
780
                      if x.needs_rename()]
781
    # these are done from longest path to shortest, to avoid deleting a
782
    # parent before its children are deleted/renamed 
783
    def longest_to_shortest(entry):
784
        path = inventory.get(entry.id)
785
        if path is None:
786
            return 0
787
        else:
788
            return len(path)
789
    my_sort(source_entries, longest_to_shortest, reverse=True)
790
791
    target_entries = source_entries[:]
792
    # These are done from shortest to longest path, to avoid creating a
793
    # child before its parent has been created/renamed
794
    def shortest_to_longest(entry):
795
        path = entry.get_new_path(inventory, changeset, reverse)
796
        if path is None:
797
            return 0
798
        else:
799
            return len(path)
800
    my_sort(target_entries, shortest_to_longest)
801
    return (source_entries, target_entries)
802
850 by Martin Pool
- Merge merge updates from aaron
803
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir, 
804
                          conflict_handler, reverse):
493 by Martin Pool
- Merge aaron's merge command
805
    """Delete and rename entries as appropriate.  Entries are renamed to temp
850 by Martin Pool
- Merge merge updates from aaron
806
    names.  A map of id -> temp name (or None, for deletions) is returned.
493 by Martin Pool
- Merge aaron's merge command
807
808
    :param source_entries: The entries to rename and delete
809
    :type source_entries: List of `ChangesetEntry`
810
    :param inventory: The map of id -> filename in the current tree
811
    :type inventory: Dictionary
812
    :param dir: The directory to apply changes to
813
    :type dir: str
814
    :param reverse: Apply changes in reverse
815
    :type reverse: bool
816
    :return: a mapping of id to temporary name
817
    :rtype: Dictionary
818
    """
819
    temp_name = {}
820
    for i in range(len(source_entries)):
821
        entry = source_entries[i]
822
        if entry.is_deletion(reverse):
823
            path = os.path.join(dir, inventory[entry.id])
824
            entry.apply(path, conflict_handler, reverse)
850 by Martin Pool
- Merge merge updates from aaron
825
            temp_name[entry.id] = None
493 by Martin Pool
- Merge aaron's merge command
826
827
        else:
850 by Martin Pool
- Merge merge updates from aaron
828
            to_name = os.path.join(temp_dir, str(i))
493 by Martin Pool
- Merge aaron's merge command
829
            src_path = inventory.get(entry.id)
830
            if src_path is not None:
831
                src_path = os.path.join(dir, src_path)
832
                try:
1185.1.40 by Robert Collins
Merge what applied of Alexander Belchenko's win32 patch.
833
                    rename(src_path, to_name)
493 by Martin Pool
- Merge aaron's merge command
834
                    temp_name[entry.id] = to_name
835
                except OSError, e:
836
                    if e.errno != errno.ENOENT:
837
                        raise
838
                    if conflict_handler.missing_for_rename(src_path) == "skip":
839
                        continue
840
841
    return temp_name
842
843
850 by Martin Pool
- Merge merge updates from aaron
844
def rename_to_new_create(changed_inventory, target_entries, inventory, 
845
                         changeset, dir, conflict_handler, reverse):
493 by Martin Pool
- Merge aaron's merge command
846
    """Rename entries with temp names to their final names, create new files.
847
850 by Martin Pool
- Merge merge updates from aaron
848
    :param changed_inventory: A mapping of id to temporary name
849
    :type changed_inventory: Dictionary
493 by Martin Pool
- Merge aaron's merge command
850
    :param target_entries: The entries to apply changes to
851
    :type target_entries: List of `ChangesetEntry`
852
    :param changeset: The changeset to apply
853
    :type changeset: `Changeset`
854
    :param dir: The directory to apply changes to
855
    :type dir: str
856
    :param reverse: If true, apply changes in reverse
857
    :type reverse: bool
858
    """
859
    for entry in target_entries:
850 by Martin Pool
- Merge merge updates from aaron
860
        new_tree_path = entry.get_new_path(inventory, changeset, reverse)
861
        if new_tree_path is None:
493 by Martin Pool
- Merge aaron's merge command
862
            continue
850 by Martin Pool
- Merge merge updates from aaron
863
        new_path = os.path.join(dir, new_tree_path)
864
        old_path = changed_inventory.get(entry.id)
493 by Martin Pool
- Merge aaron's merge command
865
        if os.path.exists(new_path):
866
            if conflict_handler.target_exists(entry, new_path, old_path) == \
867
                "skip":
868
                continue
869
        if entry.is_creation(reverse):
870
            entry.apply(new_path, conflict_handler, reverse)
850 by Martin Pool
- Merge merge updates from aaron
871
            changed_inventory[entry.id] = new_tree_path
493 by Martin Pool
- Merge aaron's merge command
872
        else:
873
            if old_path is None:
874
                continue
875
            try:
1185.1.40 by Robert Collins
Merge what applied of Alexander Belchenko's win32 patch.
876
                rename(old_path, new_path)
850 by Martin Pool
- Merge merge updates from aaron
877
                changed_inventory[entry.id] = new_tree_path
493 by Martin Pool
- Merge aaron's merge command
878
            except OSError, e:
879
                raise Exception ("%s is missing" % new_path)
880
881
class TargetExists(Exception):
882
    def __init__(self, entry, target):
883
        msg = "The path %s already exists" % target
884
        Exception.__init__(self, msg)
885
        self.entry = entry
886
        self.target = target
887
888
class RenameConflict(Exception):
889
    def __init__(self, id, this_name, base_name, other_name):
890
        msg = """Trees all have different names for a file
891
 this: %s
892
 base: %s
893
other: %s
894
   id: %s""" % (this_name, base_name, other_name, id)
895
        Exception.__init__(self, msg)
896
        self.this_name = this_name
897
        self.base_name = base_name
898
        self_other_name = other_name
899
900
class MoveConflict(Exception):
901
    def __init__(self, id, this_parent, base_parent, other_parent):
902
        msg = """The file is in different directories in every tree
903
 this: %s
904
 base: %s
905
other: %s
906
   id: %s""" % (this_parent, base_parent, other_parent, id)
907
        Exception.__init__(self, msg)
908
        self.this_parent = this_parent
909
        self.base_parent = base_parent
910
        self_other_parent = other_parent
911
912
class MergeConflict(Exception):
913
    def __init__(self, this_path):
914
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
915
        self.this_path = this_path
916
917
class MergePermissionConflict(Exception):
918
    def __init__(self, this_path, base_path, other_path):
919
        this_perms = os.stat(this_path).st_mode & 0755
920
        base_perms = os.stat(base_path).st_mode & 0755
921
        other_perms = os.stat(other_path).st_mode & 0755
922
        msg = """Conflicting permission for %s
923
this: %o
924
base: %o
925
other: %o
926
        """ % (this_path, this_perms, base_perms, other_perms)
927
        self.this_path = this_path
928
        self.base_path = base_path
929
        self.other_path = other_path
930
        Exception.__init__(self, msg)
931
932
class WrongOldContents(Exception):
933
    def __init__(self, filename):
934
        msg = "Contents mismatch deleting %s" % filename
935
        self.filename = filename
936
        Exception.__init__(self, msg)
937
938
class WrongOldPermissions(Exception):
939
    def __init__(self, filename, old_perms, new_perms):
940
        msg = "Permission missmatch on %s:\n" \
941
        "Expected 0%o, got 0%o." % (filename, old_perms, new_perms)
942
        self.filename = filename
943
        Exception.__init__(self, msg)
944
945
class RemoveContentsConflict(Exception):
946
    def __init__(self, filename):
947
        msg = "Conflict deleting %s, which has different contents in BASE"\
948
            " and THIS" % filename
949
        self.filename = filename
950
        Exception.__init__(self, msg)
951
952
class DeletingNonEmptyDirectory(Exception):
953
    def __init__(self, filename):
954
        msg = "Trying to remove dir %s while it still had files" % filename
955
        self.filename = filename
956
        Exception.__init__(self, msg)
957
958
959
class PatchTargetMissing(Exception):
960
    def __init__(self, filename):
961
        msg = "Attempt to patch %s, which does not exist" % filename
962
        Exception.__init__(self, msg)
963
        self.filename = filename
964
965
class MissingPermsFile(Exception):
966
    def __init__(self, filename):
967
        msg = "Attempt to change permissions on  %s, which does not exist" %\
968
            filename
969
        Exception.__init__(self, msg)
970
        self.filename = filename
971
972
class MissingForRm(Exception):
973
    def __init__(self, filename):
974
        msg = "Attempt to remove missing path %s" % filename
975
        Exception.__init__(self, msg)
976
        self.filename = filename
977
978
979
class MissingForRename(Exception):
980
    def __init__(self, filename):
981
        msg = "Attempt to move missing path %s" % (filename)
982
        Exception.__init__(self, msg)
983
        self.filename = filename
984
850 by Martin Pool
- Merge merge updates from aaron
985
class NewContentsConflict(Exception):
986
    def __init__(self, filename):
987
        msg = "Conflicting contents for new file %s" % (filename)
988
        Exception.__init__(self, msg)
989
990
991
class MissingForMerge(Exception):
992
    def __init__(self, filename):
993
        msg = "The file %s was modified, but does not exist in this tree"\
994
            % (filename)
995
        Exception.__init__(self, msg)
996
997
558 by Martin Pool
- All top-level classes inherit from object
998
class ExceptionConflictHandler(object):
974.1.26 by aaron.bentley at utoronto
merged mbp@sourcefrog.net-20050817233101-0939da1cf91f2472
999
    """Default handler for merge exceptions.
1000
1001
    This throws an error on any kind of conflict.  Conflict handlers can
1002
    descend from this class if they have a better way to handle some or
1003
    all types of conflict.
1004
    """
493 by Martin Pool
- Merge aaron's merge command
1005
    def missing_parent(self, pathname):
1006
        parent = os.path.dirname(pathname)
1007
        raise Exception("Parent directory missing for %s" % pathname)
1008
1009
    def dir_exists(self, pathname):
1010
        raise Exception("Directory already exists for %s" % pathname)
1011
1012
    def failed_hunks(self, pathname):
1013
        raise Exception("Failed to apply some hunks for %s" % pathname)
1014
1015
    def target_exists(self, entry, target, old_path):
1016
        raise TargetExists(entry, target)
1017
1018
    def rename_conflict(self, id, this_name, base_name, other_name):
1019
        raise RenameConflict(id, this_name, base_name, other_name)
1020
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
1021
    def move_conflict(self, id, this_dir, base_dir, other_dir):
493 by Martin Pool
- Merge aaron's merge command
1022
        raise MoveConflict(id, this_dir, base_dir, other_dir)
1023
974.1.23 by Aaron Bentley
Avoided unnecessary temp files
1024
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
493 by Martin Pool
- Merge aaron's merge command
1025
        os.unlink(new_file)
1026
        raise MergeConflict(this_path)
1027
1028
    def permission_conflict(self, this_path, base_path, other_path):
1029
        raise MergePermissionConflict(this_path, base_path, other_path)
1030
1031
    def wrong_old_contents(self, filename, expected_contents):
1032
        raise WrongOldContents(filename)
1033
1034
    def rem_contents_conflict(self, filename, this_contents, base_contents):
1035
        raise RemoveContentsConflict(filename)
1036
1037
    def wrong_old_perms(self, filename, old_perms, new_perms):
1038
        raise WrongOldPermissions(filename, old_perms, new_perms)
1039
1040
    def rmdir_non_empty(self, filename):
1041
        raise DeletingNonEmptyDirectory(filename)
1042
1043
    def link_name_exists(self, filename):
1044
        raise TargetExists(filename)
1045
1046
    def patch_target_missing(self, filename, contents):
1047
        raise PatchTargetMissing(filename)
1048
1049
    def missing_for_chmod(self, filename):
1050
        raise MissingPermsFile(filename)
1051
1052
    def missing_for_rm(self, filename, change):
1053
        raise MissingForRm(filename)
1054
1055
    def missing_for_rename(self, filename):
1056
        raise MissingForRename(filename)
1057
974.1.20 by Aaron Bentley
Eliminated ThreeWayInventory
1058
    def missing_for_merge(self, file_id, other_path):
1059
        raise MissingForMerge(other_path)
850 by Martin Pool
- Merge merge updates from aaron
1060
1061
    def new_contents_conflict(self, filename, other_contents):
1062
        raise NewContentsConflict(filename)
1063
1114 by Martin Pool
- fix bad method declaration
1064
    def finalize(self):
622 by Martin Pool
Updated merge patch from Aaron
1065
        pass
1066
493 by Martin Pool
- Merge aaron's merge command
1067
def apply_changeset(changeset, inventory, dir, conflict_handler=None, 
1068
                    reverse=False):
1069
    """Apply a changeset to a directory.
1070
1071
    :param changeset: The changes to perform
1072
    :type changeset: `Changeset`
1073
    :param inventory: The mapping of id to filename for the directory
1074
    :type inventory: Dictionary
1075
    :param dir: The path of the directory to apply the changes to
1076
    :type dir: str
1077
    :param reverse: If true, apply the changes in reverse
1078
    :type reverse: bool
1079
    :return: The mapping of the changed entries
1080
    :rtype: Dictionary
1081
    """
1082
    if conflict_handler is None:
974.1.83 by Aaron Bentley
Removed unused dir parameter from ExceptionConflictHandler
1083
        conflict_handler = ExceptionConflictHandler()
850 by Martin Pool
- Merge merge updates from aaron
1084
    temp_dir = os.path.join(dir, "bzr-tree-change")
1085
    try:
1086
        os.mkdir(temp_dir)
1087
    except OSError, e:
1088
        if e.errno == errno.EEXIST:
1089
            try:
1090
                os.rmdir(temp_dir)
1091
            except OSError, e:
1092
                if e.errno == errno.ENOTEMPTY:
1093
                    raise OldFailedTreeOp()
1094
            os.mkdir(temp_dir)
1095
        else:
1096
            raise
493 by Martin Pool
- Merge aaron's merge command
1097
    
1098
    #apply changes that don't affect filenames
1099
    for entry in changeset.entries.itervalues():
1100
        if not entry.is_creation_or_deletion():
1101
            path = os.path.join(dir, inventory[entry.id])
1102
            entry.apply(path, conflict_handler, reverse)
1103
1104
    # Apply renames in stages, to minimize conflicts:
1105
    # Only files whose name or parent change are interesting, because their
1106
    # target name may exist in the source tree.  If a directory's name changes,
1107
    # that doesn't make its children interesting.
1108
    (source_entries, target_entries) = get_rename_entries(changeset, inventory,
1109
                                                          reverse)
1110
850 by Martin Pool
- Merge merge updates from aaron
1111
    changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1112
                                              temp_dir, conflict_handler,
1113
                                              reverse)
493 by Martin Pool
- Merge aaron's merge command
1114
850 by Martin Pool
- Merge merge updates from aaron
1115
    rename_to_new_create(changed_inventory, target_entries, inventory,
1116
                         changeset, dir, conflict_handler, reverse)
493 by Martin Pool
- Merge aaron's merge command
1117
    os.rmdir(temp_dir)
850 by Martin Pool
- Merge merge updates from aaron
1118
    return changed_inventory
493 by Martin Pool
- Merge aaron's merge command
1119
1120
1121
def apply_changeset_tree(cset, tree, reverse=False):
1122
    r_inventory = {}
1123
    for entry in tree.source_inventory().itervalues():
1124
        inventory[entry.id] = entry.path
1125
    new_inventory = apply_changeset(cset, r_inventory, tree.root,
1126
                                    reverse=reverse)
1127
    new_entries, remove_entries = \
1128
        get_inventory_change(inventory, new_inventory, cset, reverse)
1129
    tree.update_source_inventory(new_entries, remove_entries)
1130
1131
1132
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
1133
    new_entries = {}
1134
    remove_entries = []
1135
    for entry in cset.entries.itervalues():
1136
        if entry.needs_rename():
850 by Martin Pool
- Merge merge updates from aaron
1137
            new_path = entry.get_new_path(inventory, cset)
1138
            if new_path is None:
1139
                remove_entries.append(entry.id)
493 by Martin Pool
- Merge aaron's merge command
1140
            else:
850 by Martin Pool
- Merge merge updates from aaron
1141
                new_entries[new_path] = entry.id
493 by Martin Pool
- Merge aaron's merge command
1142
    return new_entries, remove_entries
1143
1144
1145
def print_changeset(cset):
1146
    """Print all non-boring changeset entries
1147
    
1148
    :param cset: The changeset to print
1149
    :type cset: `Changeset`
1150
    """
1151
    for entry in cset.entries.itervalues():
1152
        if entry.is_boring():
1153
            continue
1154
        print entry.id
1155
        print entry.summarize_name(cset)
1156
1157
class CompositionFailure(Exception):
1158
    def __init__(self, old_entry, new_entry, problem):
1159
        msg = "Unable to conpose entries.\n %s" % problem
1160
        Exception.__init__(self, msg)
1161
1162
class IDMismatch(CompositionFailure):
1163
    def __init__(self, old_entry, new_entry):
1164
        problem = "Attempt to compose entries with different ids: %s and %s" %\
1165
            (old_entry.id, new_entry.id)
1166
        CompositionFailure.__init__(self, old_entry, new_entry, problem)
1167
1168
def compose_changesets(old_cset, new_cset):
1169
    """Combine two changesets into one.  This works well for exact patching.
1170
    Otherwise, not so well.
1171
1172
    :param old_cset: The first changeset that would be applied
1173
    :type old_cset: `Changeset`
1174
    :param new_cset: The second changeset that would be applied
1175
    :type new_cset: `Changeset`
1176
    :return: A changeset that combines the changes in both changesets
1177
    :rtype: `Changeset`
1178
    """
1179
    composed = Changeset()
1180
    for old_entry in old_cset.entries.itervalues():
1181
        new_entry = new_cset.entries.get(old_entry.id)
1182
        if new_entry is None:
1183
            composed.add_entry(old_entry)
1184
        else:
1185
            composed_entry = compose_entries(old_entry, new_entry)
1186
            if composed_entry.parent is not None or\
1187
                composed_entry.new_parent is not None:
1188
                composed.add_entry(composed_entry)
1189
    for new_entry in new_cset.entries.itervalues():
1190
        if not old_cset.entries.has_key(new_entry.id):
1191
            composed.add_entry(new_entry)
1192
    return composed
1193
1194
def compose_entries(old_entry, new_entry):
1195
    """Combine two entries into one.
1196
1197
    :param old_entry: The first entry that would be applied
1198
    :type old_entry: ChangesetEntry
1199
    :param old_entry: The second entry that would be applied
1200
    :type old_entry: ChangesetEntry
1201
    :return: A changeset entry combining both entries
1202
    :rtype: `ChangesetEntry`
1203
    """
1204
    if old_entry.id != new_entry.id:
1205
        raise IDMismatch(old_entry, new_entry)
1206
    output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1207
1208
    if (old_entry.parent != old_entry.new_parent or 
1209
        new_entry.parent != new_entry.new_parent):
1210
        output.new_parent = new_entry.new_parent
1211
1212
    if (old_entry.path != old_entry.new_path or 
1213
        new_entry.path != new_entry.new_path):
1214
        output.new_path = new_entry.new_path
1215
1216
    output.contents_change = compose_contents(old_entry, new_entry)
1217
    output.metadata_change = compose_metadata(old_entry, new_entry)
1218
    return output
1219
1220
def compose_contents(old_entry, new_entry):
1221
    """Combine the contents of two changeset entries.  Entries are combined
1222
    intelligently where possible, but the fallback behavior returns an 
1223
    ApplySequence.
1224
1225
    :param old_entry: The first entry that would be applied
1226
    :type old_entry: `ChangesetEntry`
1227
    :param new_entry: The second entry that would be applied
1228
    :type new_entry: `ChangesetEntry`
1229
    :return: A combined contents change
1230
    :rtype: anything supporting the apply(reverse=False) method
1231
    """
1232
    old_contents = old_entry.contents_change
1233
    new_contents = new_entry.contents_change
1234
    if old_entry.contents_change is None:
1235
        return new_entry.contents_change
1236
    elif new_entry.contents_change is None:
1237
        return old_entry.contents_change
1238
    elif isinstance(old_contents, ReplaceContents) and \
1239
        isinstance(new_contents, ReplaceContents):
1240
        if old_contents.old_contents == new_contents.new_contents:
1241
            return None
1242
        else:
1243
            return ReplaceContents(old_contents.old_contents,
1244
                                   new_contents.new_contents)
1245
    elif isinstance(old_contents, ApplySequence):
1246
        output = ApplySequence(old_contents.changes)
1247
        if isinstance(new_contents, ApplySequence):
1248
            output.changes.extend(new_contents.changes)
1249
        else:
1250
            output.changes.append(new_contents)
1251
        return output
1252
    elif isinstance(new_contents, ApplySequence):
1253
        output = ApplySequence((old_contents.changes,))
1254
        output.extend(new_contents.changes)
1255
        return output
1256
    else:
1257
        return ApplySequence((old_contents, new_contents))
1258
1259
def compose_metadata(old_entry, new_entry):
1260
    old_meta = old_entry.metadata_change
1261
    new_meta = new_entry.metadata_change
1262
    if old_meta is None:
1263
        return new_meta
1264
    elif new_meta is None:
1265
        return old_meta
1266
    elif isinstance(old_meta, ChangeUnixPermissions) and \
1267
        isinstance(new_meta, ChangeUnixPermissions):
1268
        return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1269
    else:
1270
        return ApplySequence(old_meta, new_meta)
1271
1272
1273
def changeset_is_null(changeset):
1274
    for entry in changeset.entries.itervalues():
1275
        if not entry.is_boring():
1276
            return False
1277
    return True
1278
1279
class UnsuppportedFiletype(Exception):
1280
    def __init__(self, full_path, stat_result):
1281
        msg = "The file \"%s\" is not a supported filetype." % full_path
1282
        Exception.__init__(self, msg)
1283
        self.full_path = full_path
1284
        self.stat_result = stat_result
1285
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1286
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1287
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
493 by Martin Pool
- Merge aaron's merge command
1288
1289
class ChangesetGenerator(object):
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1290
    def __init__(self, tree_a, tree_b, interesting_ids=None):
493 by Martin Pool
- Merge aaron's merge command
1291
        object.__init__(self)
1292
        self.tree_a = tree_a
1293
        self.tree_b = tree_b
974.1.17 by Aaron Bentley
Switched to using a set of interesting file_ids instead of SourceFile attribute
1294
        self._interesting_ids = interesting_ids
493 by Martin Pool
- Merge aaron's merge command
1295
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1296
    def iter_both_tree_ids(self):
1297
        for file_id in self.tree_a:
1298
            yield file_id
1299
        for file_id in self.tree_b:
1300
            if file_id not in self.tree_a:
1301
                yield file_id
493 by Martin Pool
- Merge aaron's merge command
1302
1303
    def __call__(self):
1304
        cset = Changeset()
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1305
        for file_id in self.iter_both_tree_ids():
1306
            cs_entry = self.make_entry(file_id)
493 by Martin Pool
- Merge aaron's merge command
1307
            if cs_entry is not None and not cs_entry.is_boring():
1308
                cset.add_entry(cs_entry)
1309
1310
        for entry in list(cset.entries.itervalues()):
1311
            if entry.parent != entry.new_parent:
1312
                if not cset.entries.has_key(entry.parent) and\
1313
                    entry.parent != NULL_ID and entry.parent is not None:
1314
                    parent_entry = self.make_boring_entry(entry.parent)
1315
                    cset.add_entry(parent_entry)
1316
                if not cset.entries.has_key(entry.new_parent) and\
1317
                    entry.new_parent != NULL_ID and \
1318
                    entry.new_parent is not None:
1319
                    parent_entry = self.make_boring_entry(entry.new_parent)
1320
                    cset.add_entry(parent_entry)
1321
        return cset
1322
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1323
    def iter_inventory(self, tree):
1324
        for file_id in tree:
1325
            yield self.get_entry(file_id, tree)
1326
1327
    def get_entry(self, file_id, tree):
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
1328
        if not tree.has_or_had_id(file_id):
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1329
            return None
1330
        return tree.tree.inventory[file_id]
1331
1332
    def get_entry_parent(self, entry):
1069 by Martin Pool
- merge merge improvements from aaron
1333
        if entry is None:
1334
            return None
974.1.18 by Aaron Bentley
Stopped using SourceFile in inventory
1335
        return entry.parent_id
1336
1337
    def get_path(self, file_id, tree):
974.1.47 by Aaron Bentley
Merged changes from the merge4 branch
1338
        if not tree.has_or_had_id(file_id):
974.1.18 by Aaron Bentley
Stopped using SourceFile in inventory
1339
            return None
1340
        path = tree.id2path(file_id)
1341
        if path == '':
1342
            return './.'
1343
        else:
1344
            return path
1345
1346
    def make_basic_entry(self, file_id, only_interesting):
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1347
        entry_a = self.get_entry(file_id, self.tree_a)
1348
        entry_b = self.get_entry(file_id, self.tree_b)
493 by Martin Pool
- Merge aaron's merge command
1349
        if only_interesting and not self.is_interesting(entry_a, entry_b):
974.1.12 by aaron.bentley at utoronto
Switched from text-id to hashcache for merge optimization
1350
            return None
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1351
        parent = self.get_entry_parent(entry_a)
974.1.18 by Aaron Bentley
Stopped using SourceFile in inventory
1352
        path = self.get_path(file_id, self.tree_a)
1353
        cs_entry = ChangesetEntry(file_id, parent, path)
974.1.19 by Aaron Bentley
Removed MergeTree.inventory
1354
        new_parent = self.get_entry_parent(entry_b)
1069 by Martin Pool
- merge merge improvements from aaron
1355
974.1.18 by Aaron Bentley
Stopped using SourceFile in inventory
1356
        new_path = self.get_path(file_id, self.tree_b)
493 by Martin Pool
- Merge aaron's merge command
1357
1358
        cs_entry.new_path = new_path
1359
        cs_entry.new_parent = new_parent
974.1.12 by aaron.bentley at utoronto
Switched from text-id to hashcache for merge optimization
1360
        return cs_entry
493 by Martin Pool
- Merge aaron's merge command
1361
1362
    def is_interesting(self, entry_a, entry_b):
974.1.17 by Aaron Bentley
Switched to using a set of interesting file_ids instead of SourceFile attribute
1363
        if self._interesting_ids is None:
1364
            return True
493 by Martin Pool
- Merge aaron's merge command
1365
        if entry_a is not None:
974.1.18 by Aaron Bentley
Stopped using SourceFile in inventory
1366
            file_id = entry_a.file_id
974.1.17 by Aaron Bentley
Switched to using a set of interesting file_ids instead of SourceFile attribute
1367
        elif entry_b is not None:
974.1.18 by Aaron Bentley
Stopped using SourceFile in inventory
1368
            file_id = entry_b.file_id
974.1.17 by Aaron Bentley
Switched to using a set of interesting file_ids instead of SourceFile attribute
1369
        else:
1370
            return False
1371
        return file_id in self._interesting_ids
493 by Martin Pool
- Merge aaron's merge command
1372
1373
    def make_boring_entry(self, id):
974.1.12 by aaron.bentley at utoronto
Switched from text-id to hashcache for merge optimization
1374
        cs_entry = self.make_basic_entry(id, only_interesting=False)
493 by Martin Pool
- Merge aaron's merge command
1375
        if cs_entry.is_creation_or_deletion():
1376
            return self.make_entry(id, only_interesting=False)
1377
        else:
1378
            return cs_entry
1379
        
1380
1381
    def make_entry(self, id, only_interesting=True):
974.1.12 by aaron.bentley at utoronto
Switched from text-id to hashcache for merge optimization
1382
        cs_entry = self.make_basic_entry(id, only_interesting)
493 by Martin Pool
- Merge aaron's merge command
1383
1384
        if cs_entry is None:
1385
            return None
974.1.12 by aaron.bentley at utoronto
Switched from text-id to hashcache for merge optimization
1386
        if id in self.tree_a and id in self.tree_b:
1387
            a_sha1 = self.tree_a.get_file_sha1(id)
1388
            b_sha1 = self.tree_b.get_file_sha1(id)
1389
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1390
                return cs_entry
1391
1392
        full_path_a = self.tree_a.readonly_path(id)
1393
        full_path_b = self.tree_b.readonly_path(id)
493 by Martin Pool
- Merge aaron's merge command
1394
        stat_a = self.lstat(full_path_a)
1395
        stat_b = self.lstat(full_path_b)
1396
        
1397
        cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1398
        cs_entry.contents_change = self.make_contents_change(full_path_a,
1399
                                                             stat_a, 
1400
                                                             full_path_b, 
1401
                                                             stat_b)
1402
        return cs_entry
1403
1404
    def make_mode_change(self, stat_a, stat_b):
1405
        mode_a = None
1406
        if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
1407
            mode_a = stat_a.st_mode & 0777
1408
        mode_b = None
1409
        if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
1410
            mode_b = stat_b.st_mode & 0777
1411
        if mode_a == mode_b:
1412
            return None
1413
        return ChangeUnixPermissions(mode_a, mode_b)
1414
1415
    def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
1416
        if stat_a is None and stat_b is None:
1417
            return None
1418
        if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
1419
            stat.S_ISDIR(stat_b.st_mode):
1420
            return None
1421
        if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
1422
            stat.S_ISREG(stat_b.st_mode):
1423
            if stat_a.st_ino == stat_b.st_ino and \
1424
                stat_a.st_dev == stat_b.st_dev:
1425
                return None
1426
1427
        a_contents = self.get_contents(stat_a, full_path_a)
1428
        b_contents = self.get_contents(stat_b, full_path_b)
1429
        if a_contents == b_contents:
1430
            return None
1431
        return ReplaceContents(a_contents, b_contents)
1432
1433
    def get_contents(self, stat_result, full_path):
1434
        if stat_result is None:
1435
            return None
1436
        elif stat.S_ISREG(stat_result.st_mode):
1437
            return FileCreate(file(full_path, "rb").read())
1438
        elif stat.S_ISDIR(stat_result.st_mode):
1439
            return dir_create
1440
        elif stat.S_ISLNK(stat_result.st_mode):
1441
            return SymlinkCreate(os.readlink(full_path))
1442
        else:
1443
            raise UnsupportedFiletype(full_path, stat_result)
1444
1445
    def lstat(self, full_path):
1446
        stat_result = None
1447
        if full_path is not None:
1448
            try:
1449
                stat_result = os.lstat(full_path)
1450
            except OSError, e:
1451
                if e.errno != errno.ENOENT:
1452
                    raise
1453
        return stat_result
1454
1455
1456
def full_path(entry, tree):
1457
    return os.path.join(tree.root, entry.path)
1458
1459
def new_delete_entry(entry, tree, inventory, delete):
1460
    if entry.path == "":
1461
        parent = NULL_ID
1462
    else:
1463
        parent = inventory[dirname(entry.path)].id
1464
    cs_entry = ChangesetEntry(parent, entry.path)
1465
    if delete:
1466
        cs_entry.new_path = None
1467
        cs_entry.new_parent = None
1468
    else:
1469
        cs_entry.path = None
1470
        cs_entry.parent = None
1471
    full_path = full_path(entry, tree)
1472
    status = os.lstat(full_path)
1473
    if stat.S_ISDIR(file_stat.st_mode):
1474
        action = dir_create
1475
    
1476
1477
1478
        
959 by Martin Pool
doc
1479
# XXX: Can't we unify this with the regular inventory object
558 by Martin Pool
- All top-level classes inherit from object
1480
class Inventory(object):
493 by Martin Pool
- Merge aaron's merge command
1481
    def __init__(self, inventory):
1482
        self.inventory = inventory
1483
        self.rinventory = None
1484
1485
    def get_rinventory(self):
1486
        if self.rinventory is None:
1487
            self.rinventory  = invert_dict(self.inventory)
1488
        return self.rinventory
1489
1490
    def get_path(self, id):
1491
        return self.inventory.get(id)
1492
1493
    def get_name(self, id):
850 by Martin Pool
- Merge merge updates from aaron
1494
        path = self.get_path(id)
1495
        if path is None:
1496
            return None
1497
        else:
1498
            return os.path.basename(path)
493 by Martin Pool
- Merge aaron's merge command
1499
1500
    def get_dir(self, id):
1501
        path = self.get_path(id)
1502
        if path == "":
1503
            return None
850 by Martin Pool
- Merge merge updates from aaron
1504
        if path is None:
1505
            return None
493 by Martin Pool
- Merge aaron's merge command
1506
        return os.path.dirname(path)
1507
1508
    def get_parent(self, id):
850 by Martin Pool
- Merge merge updates from aaron
1509
        if self.get_path(id) is None:
1510
            return None
493 by Martin Pool
- Merge aaron's merge command
1511
        directory = self.get_dir(id)
1512
        if directory == '.':
1513
            directory = './.'
1514
        if directory is None:
1515
            return NULL_ID
1516
        return self.get_rinventory().get(directory)