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