~bzr-pqm/bzr/bzr.dev

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