~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
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
        """
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
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:
909.1.2 by aaron.bentley at utoronto
Fixed rename handling in merge
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
        
959 by Martin Pool
doc
1532
# XXX: Can't we unify this with the regular inventory object
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)