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