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