/brz/remove-bazaar

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