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