~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

 * The internal storage of history, and logical branch identity have now
   been split into Branch, and Repository. The common locking and file 
   management routines are now in bzrlib.lockablefiles. 
   (Aaron Bentley, Robert Collins, Martin Pool)

Show diffs side-by-side

added added

removed removed

Lines of Context:
13
13
#    You should have received a copy of the GNU General Public License
14
14
#    along with this program; if not, write to the Free Software
15
15
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Represent and apply a changeset.
 
18
 
 
19
Conflicts in applying a changeset are represented as exceptions.
 
20
 
 
21
This only handles the in-memory objects representing changesets, which are
 
22
primarily used by the merge code. 
 
23
"""
 
24
 
16
25
import os.path
17
26
import errno
18
 
import patch
19
27
import stat
20
 
from bzrlib.trace import mutter
21
 
from bzrlib.osutils import rename
 
28
from shutil import rmtree
 
29
from itertools import izip
 
30
 
 
31
from bzrlib.trace import mutter, warning
 
32
from bzrlib.osutils import rename, sha_file, pathjoin, mkdtemp
22
33
import bzrlib
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
 
"""
 
34
from bzrlib.errors import BzrCheckError
32
35
 
33
36
__docformat__ = "restructuredtext"
34
37
 
 
38
 
35
39
NULL_ID = "!NULL"
36
40
 
 
41
 
37
42
class OldFailedTreeOp(Exception):
38
43
    def __init__(self):
39
44
        Exception.__init__(self, "bzr-tree-change contains files from a"
40
45
                           " previous failed merge operation.")
 
46
 
 
47
 
41
48
def invert_dict(dict):
42
49
    newdict = {}
43
50
    for (key,value) in dict.iteritems():
45
52
    return newdict
46
53
 
47
54
       
48
 
class ChangeUnixPermissions(object):
 
55
class ChangeExecFlag(object):
49
56
    """This is two-way change, suitable for file modification, creation,
50
57
    deletion"""
51
 
    def __init__(self, old_mode, new_mode):
52
 
        self.old_mode = old_mode
53
 
        self.new_mode = new_mode
 
58
    def __init__(self, old_exec_flag, new_exec_flag):
 
59
        self.old_exec_flag = old_exec_flag
 
60
        self.new_exec_flag = new_exec_flag
54
61
 
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
    def apply(self, filename, conflict_handler):
 
63
        from_exec_flag = self.old_exec_flag
 
64
        to_exec_flag = self.new_exec_flag
62
65
        try:
63
 
            current_mode = os.stat(filename).st_mode &0777
 
66
            current_exec_flag = bool(os.stat(filename).st_mode & 0111)
64
67
        except OSError, e:
65
68
            if e.errno == errno.ENOENT:
66
 
                if conflict_handler.missing_for_chmod(filename) == "skip":
 
69
                if conflict_handler.missing_for_exec_flag(filename) == "skip":
67
70
                    return
68
71
                else:
69
 
                    current_mode = from_mode
 
72
                    current_exec_flag = from_exec_flag
70
73
 
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
        if from_exec_flag is not None and current_exec_flag != from_exec_flag:
 
75
            if conflict_handler.wrong_old_exec_flag(filename,
 
76
                        from_exec_flag, current_exec_flag) != "continue":
74
77
                return
75
78
 
76
 
        if to_mode is not None:
 
79
        if to_exec_flag is not None:
 
80
            current_mode = os.stat(filename).st_mode
 
81
            if to_exec_flag:
 
82
                umask = os.umask(0)
 
83
                os.umask(umask)
 
84
                to_mode = current_mode | (0100 & ~umask)
 
85
                # Enable x-bit for others only if they can read it.
 
86
                if current_mode & 0004:
 
87
                    to_mode |= 0001 & ~umask
 
88
                if current_mode & 0040:
 
89
                    to_mode |= 0010 & ~umask
 
90
            else:
 
91
                to_mode = current_mode & ~0111
77
92
            try:
78
93
                os.chmod(filename, to_mode)
79
94
            except IOError, e:
80
95
                if e.errno == errno.ENOENT:
81
 
                    conflict_handler.missing_for_chmod(filename)
 
96
                    conflict_handler.missing_for_exec_flag(filename)
82
97
 
83
98
    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
 
99
        return (isinstance(other, ChangeExecFlag) and
 
100
                self.old_exec_flag == other.old_exec_flag and
 
101
                self.new_exec_flag == other.new_exec_flag)
92
102
 
93
103
    def __ne__(self, other):
94
104
        return not (self == other)
95
105
 
96
106
 
97
 
def dir_create(filename, conflict_handler, reverse):
 
107
def dir_create(filename, conflict_handler, reverse=False):
98
108
    """Creates the directory, or deletes it if reverse is true.  Intended to be
99
109
    used with ReplaceContents.
100
110
 
136
146
        """
137
147
        self.target = contents
138
148
 
139
 
    def __call__(self, filename, conflict_handler, reverse):
 
149
    def __repr__(self):
 
150
        return "SymlinkCreate(%s)" % self.target
 
151
 
 
152
    def __call__(self, filename, conflict_handler, reverse=False):
140
153
        """Creates or destroys the symlink.
141
154
 
142
155
        :param filename: The name of the symlink to create
165
178
    def __ne__(self, other):
166
179
        return not (self == other)
167
180
 
 
181
 
168
182
class FileCreate(object):
169
183
    """Create or delete a file (for use with ReplaceContents)"""
170
184
    def __init__(self, contents):
189
203
    def __ne__(self, other):
190
204
        return not (self == other)
191
205
 
192
 
    def __call__(self, filename, conflict_handler, reverse):
 
206
    def __call__(self, filename, conflict_handler, reverse=False):
193
207
        """Create or delete a file
194
208
 
195
209
        :param filename: The name of the file to create
221
235
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
222
236
                    return
223
237
 
224
 
                    
225
 
 
226
 
def reversed(sequence):
227
 
    max = len(sequence) - 1
228
 
    for i in range(len(sequence)):
229
 
        yield sequence[max - i]
 
238
 
 
239
class TreeFileCreate(object):
 
240
    """Create or delete a file (for use with ReplaceContents)"""
 
241
    def __init__(self, tree, file_id):
 
242
        """Constructor
 
243
 
 
244
        :param contents: The contents of the file to write
 
245
        :type contents: str
 
246
        """
 
247
        self.tree = tree
 
248
        self.file_id = file_id
 
249
 
 
250
    def __repr__(self):
 
251
        return "TreeFileCreate(%s)" % self.file_id
 
252
 
 
253
    def __eq__(self, other):
 
254
        if not isinstance(other, TreeFileCreate):
 
255
            return False
 
256
        return self.tree.get_file_sha1(self.file_id) == \
 
257
            other.tree.get_file_sha1(other.file_id)
 
258
 
 
259
    def __ne__(self, other):
 
260
        return not (self == other)
 
261
 
 
262
    def write_file(self, filename):
 
263
        outfile = file(filename, "wb")
 
264
        for line in self.tree.get_file(self.file_id):
 
265
            outfile.write(line)
 
266
 
 
267
    def same_text(self, filename):
 
268
        in_file = file(filename, "rb")
 
269
        return sha_file(in_file) == self.tree.get_file_sha1(self.file_id)
 
270
 
 
271
    def __call__(self, filename, conflict_handler, reverse=False):
 
272
        """Create or delete a file
 
273
 
 
274
        :param filename: The name of the file to create
 
275
        :type filename: str
 
276
        :param reverse: Delete the file instead of creating it
 
277
        :type reverse: bool
 
278
        """
 
279
        if not reverse:
 
280
            try:
 
281
                self.write_file(filename)
 
282
            except IOError, e:
 
283
                if e.errno == errno.ENOENT:
 
284
                    if conflict_handler.missing_parent(filename)=="continue":
 
285
                        self.write_file(filename)
 
286
                else:
 
287
                    raise
 
288
 
 
289
        else:
 
290
            try:
 
291
                if not self.same_text(filename):
 
292
                    direction = conflict_handler.wrong_old_contents(filename,
 
293
                        self.tree.get_file(self.file_id).read())
 
294
                    if  direction != "continue":
 
295
                        return
 
296
                os.unlink(filename)
 
297
            except IOError, e:
 
298
                if e.errno != errno.ENOENT:
 
299
                    raise
 
300
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
 
301
                    return
 
302
 
230
303
 
231
304
class ReplaceContents(object):
232
305
    """A contents-replacement framework.  It allows a file/directory/symlink to
264
337
    def __ne__(self, other):
265
338
        return not (self == other)
266
339
 
267
 
    def apply(self, filename, conflict_handler, reverse=False):
 
340
    def apply(self, filename, conflict_handler):
268
341
        """Applies the FileReplacement to the specified filename
269
342
 
270
343
        :param filename: The name of the file to apply changes to
271
344
        :type filename: str
272
 
        :param reverse: If true, apply the change in reverse
273
 
        :type reverse: bool
274
345
        """
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
 
346
        undo = self.old_contents
 
347
        perform = self.new_contents
281
348
        mode = None
282
349
        if undo is not None:
283
350
            try:
291
358
                    return
292
359
            undo(filename, conflict_handler, reverse=True)
293
360
        if perform is not None:
294
 
            perform(filename, conflict_handler, reverse=False)
 
361
            perform(filename, conflict_handler)
295
362
            if mode is not None:
296
363
                os.chmod(filename, mode)
297
364
 
298
 
class ApplySequence(object):
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)
 
365
    def is_creation(self):
 
366
        return self.new_contents is not None and self.old_contents is None
 
367
 
 
368
    def is_deletion(self):
 
369
        return self.old_contents is not None and self.new_contents is None
326
370
 
327
371
 
328
372
class Diff3Merge(object):
 
373
    history_based = False
329
374
    def __init__(self, file_id, base, other):
330
375
        self.file_id = file_id
331
376
        self.base = base
332
377
        self.other = other
333
378
 
 
379
    def is_creation(self):
 
380
        return False
 
381
 
 
382
    def is_deletion(self):
 
383
        return False
 
384
 
334
385
    def __eq__(self, other):
335
386
        if not isinstance(other, Diff3Merge):
336
387
            return False
340
391
    def __ne__(self, other):
341
392
        return not (self == other)
342
393
 
343
 
    def apply(self, filename, conflict_handler, reverse=False):
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)
347
 
        if not reverse:
 
394
    def dump_file(self, temp_dir, name, tree):
 
395
        out_path = pathjoin(temp_dir, name)
 
396
        out_file = file(out_path, "wb")
 
397
        in_file = tree.get_file(self.file_id)
 
398
        for line in in_file:
 
399
            out_file.write(line)
 
400
        return out_path
 
401
 
 
402
    def apply(self, filename, conflict_handler):
 
403
        import bzrlib.patch
 
404
        temp_dir = mkdtemp(prefix="bzr-", dir=os.path.dirname(filename))
 
405
        try:
 
406
            new_file = os.path.join(temp_dir, filename)
 
407
            base_file = self.dump_file(temp_dir, "base", self.base)
 
408
            other_file = self.dump_file(temp_dir, "other", self.other)
348
409
            base = base_file
349
410
            other = other_file
350
 
        else:
351
 
            base = other_file
352
 
            other = base_file
353
 
        status = patch.diff3(new_file, filename, base, other)
354
 
        if status == 0:
355
 
            os.chmod(new_file, os.stat(filename).st_mode)
356
 
            rename(new_file, filename)
357
 
            return
358
 
        else:
359
 
            assert(status == 1)
360
 
            def get_lines(filename):
361
 
                my_file = file(filename, "rb")
362
 
                lines = my_file.readlines()
363
 
                my_file.close()
364
 
                return lines
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)
 
411
            status = bzrlib.patch.diff3(new_file, filename, base, other)
 
412
            if status == 0:
 
413
                os.chmod(new_file, os.stat(filename).st_mode)
 
414
                rename(new_file, filename)
 
415
                return
 
416
            else:
 
417
                assert(status == 1)
 
418
                def get_lines(filename):
 
419
                    my_file = file(filename, "rb")
 
420
                    lines = my_file.readlines()
 
421
                    my_file.close()
 
422
                    return lines
 
423
                base_lines = get_lines(base)
 
424
                other_lines = get_lines(other)
 
425
                conflict_handler.merge_conflict(new_file, filename, base_lines, 
 
426
                                                other_lines)
 
427
        finally:
 
428
            rmtree(temp_dir)
369
429
 
370
430
 
371
431
def CreateDir():
376
436
    """
377
437
    return ReplaceContents(None, dir_create)
378
438
 
 
439
 
379
440
def DeleteDir():
380
441
    """Convenience function to delete a directory.
381
442
 
384
445
    """
385
446
    return ReplaceContents(dir_create, None)
386
447
 
 
448
 
387
449
def CreateFile(contents):
388
450
    """Convenience fucntion to create a file.
389
451
    
394
456
    """
395
457
    return ReplaceContents(None, FileCreate(contents))
396
458
 
 
459
 
397
460
def DeleteFile(contents):
398
461
    """Convenience fucntion to delete a file.
399
462
    
404
467
    """
405
468
    return ReplaceContents(FileCreate(contents), None)
406
469
 
407
 
def ReplaceFileContents(old_contents, new_contents):
 
470
 
 
471
def ReplaceFileContents(old_tree, new_tree, file_id):
408
472
    """Convenience fucntion to replace the contents of a file.
409
473
    
410
474
    :param old_contents: The contents of the file to replace 
414
478
    :return: A ReplaceContents that will replace the contents of a file a file 
415
479
    :rtype: `ReplaceContents`
416
480
    """
417
 
    return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
 
481
    return ReplaceContents(TreeFileCreate(old_tree, file_id), 
 
482
                           TreeFileCreate(new_tree, file_id))
 
483
 
418
484
 
419
485
def CreateSymlink(target):
420
486
    """Convenience fucntion to create a symlink.
426
492
    """
427
493
    return ReplaceContents(None, SymlinkCreate(target))
428
494
 
 
495
 
429
496
def DeleteSymlink(target):
430
497
    """Convenience fucntion to delete a symlink.
431
498
    
436
503
    """
437
504
    return ReplaceContents(SymlinkCreate(target), None)
438
505
 
 
506
 
439
507
def ChangeTarget(old_target, new_target):
440
508
    """Convenience fucntion to change the target of a symlink.
441
509
    
479
547
        msg = 'Child of !NULL is named "%s", not "./.".' % name
480
548
        InvalidEntry.__init__(self, entry, msg)
481
549
 
 
550
 
482
551
class NullIDAssigned(InvalidEntry):
483
552
    """The id !NULL was assigned to a real entry"""
484
553
    def __init__(self, entry):
490
559
        msg = '"!NULL" id assigned to a file "%s".' % entry.path
491
560
        InvalidEntry.__init__(self, entry, msg)
492
561
 
 
562
 
493
563
class ParentIDIsSelf(InvalidEntry):
494
564
    """An entry is marked as its own parent"""
495
565
    def __init__(self, entry):
502
572
            (entry.path, entry.id)
503
573
        InvalidEntry.__init__(self, entry, msg)
504
574
 
 
575
 
505
576
class ChangesetEntry(object):
506
577
    """An entry the changeset"""
507
578
    def __init__(self, id, parent, path):
526
597
        if self.id  == self.parent:
527
598
            raise ParentIDIsSelf(self)
528
599
 
529
 
    def __str__(self):
 
600
    def __repr__(self):
530
601
        return "ChangesetEntry(%s)" % self.id
531
602
 
 
603
    __str__ = __repr__
 
604
 
532
605
    def __get_dir(self):
533
606
        if self.path is None:
534
607
            return None
535
608
        return os.path.dirname(self.path)
536
609
 
537
610
    def __set_dir(self, dir):
538
 
        self.path = os.path.join(dir, os.path.basename(self.path))
 
611
        self.path = pathjoin(dir, os.path.basename(self.path))
539
612
 
540
613
    dir = property(__get_dir, __set_dir)
541
614
    
545
618
        return os.path.basename(self.path)
546
619
 
547
620
    def __set_name(self, name):
548
 
        self.path = os.path.join(os.path.dirname(self.path), name)
 
621
        self.path = pathjoin(os.path.dirname(self.path), name)
549
622
 
550
623
    name = property(__get_name, __set_name)
551
624
 
555
628
        return os.path.dirname(self.new_path)
556
629
 
557
630
    def __set_new_dir(self, dir):
558
 
        self.new_path = os.path.join(dir, os.path.basename(self.new_path))
 
631
        self.new_path = pathjoin(dir, os.path.basename(self.new_path))
559
632
 
560
633
    new_dir = property(__get_new_dir, __set_new_dir)
561
634
 
565
638
        return os.path.basename(self.new_path)
566
639
 
567
640
    def __set_new_name(self, name):
568
 
        self.new_path = os.path.join(os.path.dirname(self.new_path), name)
 
641
        self.new_path = pathjoin(os.path.dirname(self.new_path), name)
569
642
 
570
643
    new_name = property(__get_new_name, __set_new_name)
571
644
 
577
650
 
578
651
        return (self.parent != self.new_parent or self.name != self.new_name)
579
652
 
580
 
    def is_deletion(self, reverse):
 
653
    def is_deletion(self, reverse=False):
581
654
        """Return true if applying the entry would delete a file/directory.
582
655
 
583
656
        :param reverse: if true, the changeset is being applied in reverse
584
657
        :rtype: bool
585
658
        """
586
 
        return ((self.new_parent is None and not reverse) or 
587
 
                (self.parent is None and reverse))
 
659
        return self.is_creation(not reverse)
588
660
 
589
 
    def is_creation(self, reverse):
 
661
    def is_creation(self, reverse=False):
590
662
        """Return true if applying the entry would create a file/directory.
591
663
 
592
664
        :param reverse: if true, the changeset is being applied in reverse
593
665
        :rtype: bool
594
666
        """
595
 
        return ((self.parent is None and not reverse) or 
596
 
                (self.new_parent is None and reverse))
 
667
        if self.contents_change is None:
 
668
            return False
 
669
        if reverse:
 
670
            return self.contents_change.is_deletion()
 
671
        else:
 
672
            return self.contents_change.is_creation()
597
673
 
598
674
    def is_creation_or_deletion(self):
599
675
        """Return true if applying the entry would create or delete a 
601
677
 
602
678
        :rtype: bool
603
679
        """
604
 
        return self.parent is None or self.new_parent is None
 
680
        return self.is_creation() or self.is_deletion()
605
681
 
606
682
    def get_cset_path(self, mod=False):
607
683
        """Determine the path of the entry according to the changeset.
627
703
                return None
628
704
            return self.path
629
705
 
630
 
    def summarize_name(self, reverse=False):
 
706
    def summarize_name(self):
631
707
        """Produce a one-line summary of the filename.  Indicates renames as
632
708
        old => new, indicates creation as None => new, indicates deletion as
633
709
        old => None.
634
710
 
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
711
        :rtype: str
640
712
        """
641
713
        orig_path = self.get_cset_path(False)
642
714
        mod_path = self.get_cset_path(True)
643
 
        if orig_path is not None:
 
715
        if orig_path and orig_path.startswith('./'):
644
716
            orig_path = orig_path[2:]
645
 
        if mod_path is not None:
 
717
        if mod_path and mod_path.startswith('./'):
646
718
            mod_path = mod_path[2:]
647
719
        if orig_path == mod_path:
648
720
            return orig_path
649
721
        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):
 
722
            return "%s => %s" % (orig_path, mod_path)
 
723
 
 
724
    def get_new_path(self, id_map, changeset):
657
725
        """Determine the full pathname to rename to
658
726
 
659
727
        :param id_map: The map of ids to filenames for the tree
660
728
        :type id_map: Dictionary
661
729
        :param changeset: The changeset to get data from
662
730
        :type changeset: `Changeset`
663
 
        :param reverse: If true, we're applying the changeset in reverse
664
 
        :type reverse: bool
665
731
        :rtype: str
666
732
        """
667
 
        mutter("Finding new path for %s" % self.summarize_name())
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
 
733
        mutter("Finding new path for %s", self.summarize_name())
 
734
        parent = self.new_parent
 
735
        to_dir = self.new_dir
 
736
        from_dir = self.dir
 
737
        to_name = self.new_name
 
738
        from_name = self.name
680
739
 
681
740
        if to_name is None:
682
741
            return None
683
742
 
684
743
        if parent == NULL_ID or parent is None:
685
 
            if to_name != '.':
 
744
            if to_name != u'.':
686
745
                raise SourceRootHasName(self, to_name)
687
746
            else:
688
 
                return '.'
689
 
        if from_dir == to_dir:
 
747
                return u'.'
 
748
        parent_entry = changeset.entries.get(parent)
 
749
        if parent_entry is None:
690
750
            dir = os.path.dirname(id_map[self.id])
691
751
        else:
692
 
            mutter("path, new_path: %r %r" % (self.path, self.new_path))
693
 
            parent_entry = changeset.entries[parent]
694
 
            dir = parent_entry.get_new_path(id_map, changeset, reverse)
 
752
            mutter("path, new_path: %r %r", self.path, self.new_path)
 
753
            dir = parent_entry.get_new_path(id_map, changeset)
695
754
        if from_name == to_name:
696
755
            name = os.path.basename(id_map[self.id])
697
756
        else:
698
757
            name = to_name
699
758
            assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
700
 
        return os.path.join(dir, name)
 
759
        return pathjoin(dir, name)
701
760
 
702
761
    def is_boring(self):
703
762
        """Determines whether the entry does nothing
716
775
        else:
717
776
            return True
718
777
 
719
 
    def apply(self, filename, conflict_handler, reverse=False):
 
778
    def apply(self, filename, conflict_handler):
720
779
        """Applies the file content and/or metadata changes.
721
780
 
722
781
        :param filename: the filename of the entry
723
782
        :type filename: str
724
 
        :param reverse: If true, apply the changes in reverse
725
 
        :type reverse: bool
726
783
        """
727
 
        if self.is_deletion(reverse) and self.metadata_change is not None:
728
 
            self.metadata_change.apply(filename, conflict_handler, reverse)
 
784
        if self.is_deletion() and self.metadata_change is not None:
 
785
            self.metadata_change.apply(filename, conflict_handler)
729
786
        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)
 
787
            self.contents_change.apply(filename, conflict_handler)
 
788
        if not self.is_deletion() and self.metadata_change is not None:
 
789
            self.metadata_change.apply(filename, conflict_handler)
 
790
 
733
791
 
734
792
class IDPresent(Exception):
735
793
    def __init__(self, id):
738
796
        Exception.__init__(self, msg)
739
797
        self.id = id
740
798
 
 
799
 
741
800
class Changeset(object):
742
801
    """A set of changes to apply"""
743
802
    def __init__(self):
749
808
            raise IDPresent(entry.id)
750
809
        self.entries[entry.id] = entry
751
810
 
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
811
 
768
 
def get_rename_entries(changeset, inventory, reverse):
 
812
def get_rename_entries(changeset, inventory):
769
813
    """Return a list of entries that will be renamed.  Entries are sorted from
770
814
    longest to shortest source path and from shortest to longest target path.
771
815
 
773
817
    :type changeset: `Changeset`
774
818
    :param inventory: The source of current tree paths for the given ids
775
819
    :type inventory: Dictionary
776
 
    :param reverse: If true, the changeset is being applied in reverse
777
 
    :type reverse: bool
778
820
    :return: source entries and target entries as a tuple
779
821
    :rtype: (List, List)
780
822
    """
781
823
    source_entries = [x for x in changeset.entries.itervalues() 
782
 
                      if x.needs_rename()]
 
824
                      if x.needs_rename() or x.is_creation_or_deletion()]
783
825
    # these are done from longest path to shortest, to avoid deleting a
784
826
    # parent before its children are deleted/renamed 
785
827
    def longest_to_shortest(entry):
788
830
            return 0
789
831
        else:
790
832
            return len(path)
791
 
    my_sort(source_entries, longest_to_shortest, reverse=True)
 
833
    source_entries.sort(None, longest_to_shortest, True)
792
834
 
793
835
    target_entries = source_entries[:]
794
836
    # These are done from shortest to longest path, to avoid creating a
795
837
    # child before its parent has been created/renamed
796
838
    def shortest_to_longest(entry):
797
 
        path = entry.get_new_path(inventory, changeset, reverse)
 
839
        path = entry.get_new_path(inventory, changeset)
798
840
        if path is None:
799
841
            return 0
800
842
        else:
801
843
            return len(path)
802
 
    my_sort(target_entries, shortest_to_longest)
 
844
    target_entries.sort(None, shortest_to_longest)
803
845
    return (source_entries, target_entries)
804
846
 
 
847
 
805
848
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir, 
806
 
                          conflict_handler, reverse):
 
849
                          conflict_handler):
807
850
    """Delete and rename entries as appropriate.  Entries are renamed to temp
808
851
    names.  A map of id -> temp name (or None, for deletions) is returned.
809
852
 
813
856
    :type inventory: Dictionary
814
857
    :param dir: The directory to apply changes to
815
858
    :type dir: str
816
 
    :param reverse: Apply changes in reverse
817
 
    :type reverse: bool
818
859
    :return: a mapping of id to temporary name
819
860
    :rtype: Dictionary
820
861
    """
821
862
    temp_name = {}
822
863
    for i in range(len(source_entries)):
823
864
        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)
 
865
        if entry.is_deletion():
 
866
            path = pathjoin(dir, inventory[entry.id])
 
867
            entry.apply(path, conflict_handler)
827
868
            temp_name[entry.id] = None
828
869
 
829
 
        else:
830
 
            to_name = os.path.join(temp_dir, str(i))
 
870
        elif entry.needs_rename():
 
871
            if entry.is_creation():
 
872
                continue
 
873
            to_name = pathjoin(temp_dir, str(i))
831
874
            src_path = inventory.get(entry.id)
832
875
            if src_path is not None:
833
 
                src_path = os.path.join(dir, src_path)
 
876
                src_path = pathjoin(dir, src_path)
834
877
                try:
835
878
                    rename(src_path, to_name)
836
879
                    temp_name[entry.id] = to_name
837
880
                except OSError, e:
838
881
                    if e.errno != errno.ENOENT:
839
882
                        raise
840
 
                    if conflict_handler.missing_for_rename(src_path) == "skip":
 
883
                    if conflict_handler.missing_for_rename(src_path, to_name) \
 
884
                        == "skip":
841
885
                        continue
842
886
 
843
887
    return temp_name
844
888
 
845
889
 
846
890
def rename_to_new_create(changed_inventory, target_entries, inventory, 
847
 
                         changeset, dir, conflict_handler, reverse):
 
891
                         changeset, dir, conflict_handler):
848
892
    """Rename entries with temp names to their final names, create new files.
849
893
 
850
894
    :param changed_inventory: A mapping of id to temporary name
855
899
    :type changeset: `Changeset`
856
900
    :param dir: The directory to apply changes to
857
901
    :type dir: str
858
 
    :param reverse: If true, apply changes in reverse
859
 
    :type reverse: bool
860
902
    """
861
903
    for entry in target_entries:
862
 
        new_tree_path = entry.get_new_path(inventory, changeset, reverse)
 
904
        new_tree_path = entry.get_new_path(inventory, changeset)
863
905
        if new_tree_path is None:
864
906
            continue
865
 
        new_path = os.path.join(dir, new_tree_path)
 
907
        new_path = pathjoin(dir, new_tree_path)
866
908
        old_path = changed_inventory.get(entry.id)
867
909
        if bzrlib.osutils.lexists(new_path):
868
910
            if conflict_handler.target_exists(entry, new_path, old_path) == \
869
911
                "skip":
870
912
                continue
871
 
        if entry.is_creation(reverse):
872
 
            entry.apply(new_path, conflict_handler, reverse)
 
913
        if entry.is_creation():
 
914
            entry.apply(new_path, conflict_handler)
873
915
            changed_inventory[entry.id] = new_tree_path
874
 
        else:
 
916
        elif entry.needs_rename():
 
917
            if entry.is_deletion():
 
918
                continue
875
919
            if old_path is None:
876
920
                continue
877
921
            try:
 
922
                mutter('rename %s to final name %s', old_path, new_path)
878
923
                rename(old_path, new_path)
879
924
                changed_inventory[entry.id] = new_tree_path
880
925
            except OSError, e:
881
 
                raise Exception ("%s is missing" % new_path)
 
926
                raise BzrCheckError('failed to rename %s to %s for changeset entry %s: %s'
 
927
                        % (old_path, new_path, entry, e))
 
928
 
882
929
 
883
930
class TargetExists(Exception):
884
931
    def __init__(self, entry, target):
887
934
        self.entry = entry
888
935
        self.target = target
889
936
 
 
937
 
890
938
class RenameConflict(Exception):
891
939
    def __init__(self, id, this_name, base_name, other_name):
892
940
        msg = """Trees all have different names for a file
899
947
        self.base_name = base_name
900
948
        self_other_name = other_name
901
949
 
 
950
 
902
951
class MoveConflict(Exception):
903
952
    def __init__(self, id, this_parent, base_parent, other_parent):
904
953
        msg = """The file is in different directories in every tree
911
960
        self.base_parent = base_parent
912
961
        self_other_parent = other_parent
913
962
 
 
963
 
914
964
class MergeConflict(Exception):
915
965
    def __init__(self, this_path):
916
966
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
917
967
        self.this_path = this_path
918
968
 
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
969
 
934
970
class WrongOldContents(Exception):
935
971
    def __init__(self, filename):
937
973
        self.filename = filename
938
974
        Exception.__init__(self, msg)
939
975
 
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)
 
976
 
 
977
class WrongOldExecFlag(Exception):
 
978
    def __init__(self, filename, old_exec_flag, new_exec_flag):
 
979
        msg = "Executable flag missmatch on %s:\n" \
 
980
        "Expected %s, got %s." % (filename, old_exec_flag, new_exec_flag)
944
981
        self.filename = filename
945
982
        Exception.__init__(self, msg)
946
983
 
 
984
 
947
985
class RemoveContentsConflict(Exception):
948
986
    def __init__(self, filename):
949
987
        msg = "Conflict deleting %s, which has different contents in BASE"\
951
989
        self.filename = filename
952
990
        Exception.__init__(self, msg)
953
991
 
 
992
 
954
993
class DeletingNonEmptyDirectory(Exception):
955
994
    def __init__(self, filename):
956
995
        msg = "Trying to remove dir %s while it still had files" % filename
964
1003
        Exception.__init__(self, msg)
965
1004
        self.filename = filename
966
1005
 
967
 
class MissingPermsFile(Exception):
 
1006
 
 
1007
class MissingForSetExec(Exception):
968
1008
    def __init__(self, filename):
969
1009
        msg = "Attempt to change permissions on  %s, which does not exist" %\
970
1010
            filename
971
1011
        Exception.__init__(self, msg)
972
1012
        self.filename = filename
973
1013
 
 
1014
 
974
1015
class MissingForRm(Exception):
975
1016
    def __init__(self, filename):
976
1017
        msg = "Attempt to remove missing path %s" % filename
979
1020
 
980
1021
 
981
1022
class MissingForRename(Exception):
982
 
    def __init__(self, filename):
983
 
        msg = "Attempt to move missing path %s" % (filename)
 
1023
    def __init__(self, filename, to_path):
 
1024
        msg = "Attempt to move missing path %s to %s" % (filename, to_path)
984
1025
        Exception.__init__(self, msg)
985
1026
        self.filename = filename
986
1027
 
 
1028
 
987
1029
class NewContentsConflict(Exception):
988
1030
    def __init__(self, filename):
989
1031
        msg = "Conflicting contents for new file %s" % (filename)
990
1032
        Exception.__init__(self, msg)
991
1033
 
992
1034
 
 
1035
class WeaveMergeConflict(Exception):
 
1036
    def __init__(self, filename):
 
1037
        msg = "Conflicting contents for file %s" % (filename)
 
1038
        Exception.__init__(self, msg)
 
1039
 
 
1040
 
 
1041
class ThreewayContentsConflict(Exception):
 
1042
    def __init__(self, filename):
 
1043
        msg = "Conflicting contents for file %s" % (filename)
 
1044
        Exception.__init__(self, msg)
 
1045
 
 
1046
 
993
1047
class MissingForMerge(Exception):
994
1048
    def __init__(self, filename):
995
1049
        msg = "The file %s was modified, but does not exist in this tree"\
1027
1081
        os.unlink(new_file)
1028
1082
        raise MergeConflict(this_path)
1029
1083
 
1030
 
    def permission_conflict(self, this_path, base_path, other_path):
1031
 
        raise MergePermissionConflict(this_path, base_path, other_path)
1032
 
 
1033
1084
    def wrong_old_contents(self, filename, expected_contents):
1034
1085
        raise WrongOldContents(filename)
1035
1086
 
1036
1087
    def rem_contents_conflict(self, filename, this_contents, base_contents):
1037
1088
        raise RemoveContentsConflict(filename)
1038
1089
 
1039
 
    def wrong_old_perms(self, filename, old_perms, new_perms):
1040
 
        raise WrongOldPermissions(filename, old_perms, new_perms)
 
1090
    def wrong_old_exec_flag(self, filename, old_exec_flag, new_exec_flag):
 
1091
        raise WrongOldExecFlag(filename, old_exec_flag, new_exec_flag)
1041
1092
 
1042
1093
    def rmdir_non_empty(self, filename):
1043
1094
        raise DeletingNonEmptyDirectory(filename)
1048
1099
    def patch_target_missing(self, filename, contents):
1049
1100
        raise PatchTargetMissing(filename)
1050
1101
 
1051
 
    def missing_for_chmod(self, filename):
1052
 
        raise MissingPermsFile(filename)
 
1102
    def missing_for_exec_flag(self, filename):
 
1103
        raise MissingForExecFlag(filename)
1053
1104
 
1054
1105
    def missing_for_rm(self, filename, change):
1055
1106
        raise MissingForRm(filename)
1056
1107
 
1057
 
    def missing_for_rename(self, filename):
1058
 
        raise MissingForRename(filename)
 
1108
    def missing_for_rename(self, filename, to_path):
 
1109
        raise MissingForRename(filename, to_path)
1059
1110
 
1060
1111
    def missing_for_merge(self, file_id, other_path):
1061
1112
        raise MissingForMerge(other_path)
1063
1114
    def new_contents_conflict(self, filename, other_contents):
1064
1115
        raise NewContentsConflict(filename)
1065
1116
 
 
1117
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
 
1118
        raise WeaveMergeConflict(filename)
 
1119
 
 
1120
    def threeway_contents_conflict(self, filename, this_contents,
 
1121
                                   base_contents, other_contents):
 
1122
        raise ThreewayContentsConflict(filename)
 
1123
 
1066
1124
    def finalize(self):
1067
1125
        pass
1068
1126
 
1069
 
def apply_changeset(changeset, inventory, dir, conflict_handler=None, 
1070
 
                    reverse=False):
 
1127
 
 
1128
def apply_changeset(changeset, inventory, dir, conflict_handler=None):
1071
1129
    """Apply a changeset to a directory.
1072
1130
 
1073
1131
    :param changeset: The changes to perform
1076
1134
    :type inventory: Dictionary
1077
1135
    :param dir: The path of the directory to apply the changes to
1078
1136
    :type dir: str
1079
 
    :param reverse: If true, apply the changes in reverse
1080
 
    :type reverse: bool
1081
1137
    :return: The mapping of the changed entries
1082
1138
    :rtype: Dictionary
1083
1139
    """
1084
1140
    if conflict_handler is None:
1085
1141
        conflict_handler = ExceptionConflictHandler()
1086
 
    temp_dir = os.path.join(dir, "bzr-tree-change")
 
1142
    temp_dir = pathjoin(dir, "bzr-tree-change")
1087
1143
    try:
1088
1144
        os.mkdir(temp_dir)
1089
1145
    except OSError, e:
1099
1155
    
1100
1156
    #apply changes that don't affect filenames
1101
1157
    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)
 
1158
        if not entry.is_creation_or_deletion() and not entry.is_boring():
 
1159
            if entry.id not in inventory:
 
1160
                warning("entry {%s} no longer present, can't be updated",
 
1161
                        entry.id)
 
1162
                continue
 
1163
            path = pathjoin(dir, inventory[entry.id])
 
1164
            entry.apply(path, conflict_handler)
1105
1165
 
1106
1166
    # Apply renames in stages, to minimize conflicts:
1107
1167
    # Only files whose name or parent change are interesting, because their
1108
1168
    # target name may exist in the source tree.  If a directory's name changes,
1109
1169
    # that doesn't make its children interesting.
1110
 
    (source_entries, target_entries) = get_rename_entries(changeset, inventory,
1111
 
                                                          reverse)
 
1170
    (source_entries, target_entries) = get_rename_entries(changeset, inventory)
1112
1171
 
1113
1172
    changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1114
 
                                              temp_dir, conflict_handler,
1115
 
                                              reverse)
 
1173
                                              temp_dir, conflict_handler)
1116
1174
 
1117
1175
    rename_to_new_create(changed_inventory, target_entries, inventory,
1118
 
                         changeset, dir, conflict_handler, reverse)
 
1176
                         changeset, dir, conflict_handler)
1119
1177
    os.rmdir(temp_dir)
1120
1178
    return changed_inventory
1121
1179
 
1122
1180
 
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():
1139
 
            new_path = entry.get_new_path(inventory, cset)
1140
 
            if new_path is None:
1141
 
                remove_entries.append(entry.id)
1142
 
            else:
1143
 
                new_entries[new_path] = entry.id
1144
 
    return new_entries, remove_entries
1145
 
 
1146
 
 
1147
1181
def print_changeset(cset):
1148
1182
    """Print all non-boring changeset entries
1149
1183
    
1156
1190
        print entry.id
1157
1191
        print entry.summarize_name(cset)
1158
1192
 
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
 
1193
 
 
1194
class UnsupportedFiletype(Exception):
 
1195
    def __init__(self, kind, full_path):
 
1196
        msg = "The file \"%s\" is a %s, which is not a supported filetype." \
 
1197
            % (full_path, kind)
1284
1198
        Exception.__init__(self, msg)
1285
1199
        self.full_path = full_path
1286
 
        self.stat_result = stat_result
 
1200
        self.kind = kind
 
1201
 
1287
1202
 
1288
1203
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1289
1204
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1290
1205
 
 
1206
 
1291
1207
class ChangesetGenerator(object):
1292
1208
    def __init__(self, tree_a, tree_b, interesting_ids=None):
1293
1209
        object.__init__(self)
1329
1245
    def get_entry(self, file_id, tree):
1330
1246
        if not tree.has_or_had_id(file_id):
1331
1247
            return None
1332
 
        return tree.tree.inventory[file_id]
 
1248
        return tree.inventory[file_id]
1333
1249
 
1334
1250
    def get_entry_parent(self, entry):
1335
1251
        if entry is None:
1378
1294
            return self.make_entry(id, only_interesting=False)
1379
1295
        else:
1380
1296
            return cs_entry
1381
 
        
1382
1297
 
1383
1298
    def make_entry(self, id, only_interesting=True):
1384
1299
        cs_entry = self.make_basic_entry(id, only_interesting)
1386
1301
        if cs_entry is None:
1387
1302
            return None
1388
1303
 
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)
 
1304
        cs_entry.metadata_change = self.make_exec_flag_change(id)
1395
1305
 
1396
1306
        if id in self.tree_a and id in self.tree_b:
1397
1307
            a_sha1 = self.tree_a.get_file_sha1(id)
1399
1309
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1400
1310
                return cs_entry
1401
1311
 
1402
 
        cs_entry.contents_change = self.make_contents_change(full_path_a,
1403
 
                                                             stat_a, 
1404
 
                                                             full_path_b, 
1405
 
                                                             stat_b)
 
1312
        cs_entry.contents_change = self.make_contents_change(id)
1406
1313
        return cs_entry
1407
1314
 
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)
 
1315
    def make_exec_flag_change(self, file_id):
 
1316
        exec_flag_a = exec_flag_b = None
 
1317
        if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
 
1318
            exec_flag_a = self.tree_a.is_executable(file_id)
 
1319
 
 
1320
        if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
 
1321
            exec_flag_b = self.tree_b.is_executable(file_id)
 
1322
 
 
1323
        if exec_flag_a == exec_flag_b:
 
1324
            return None
 
1325
        return ChangeExecFlag(exec_flag_a, exec_flag_b)
 
1326
 
 
1327
    def make_contents_change(self, file_id):
 
1328
        a_contents = get_contents(self.tree_a, file_id)
 
1329
        b_contents = get_contents(self.tree_b, file_id)
1433
1330
        if a_contents == b_contents:
1434
1331
            return None
1435
1332
        return ReplaceContents(a_contents, b_contents)
1436
1333
 
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
1334
 
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
 
1335
def get_contents(tree, file_id):
 
1336
    """Return the appropriate contents to create a copy of file_id from tree"""
 
1337
    if file_id not in tree:
 
1338
        return None
 
1339
    kind = tree.kind(file_id)
 
1340
    if kind == "file":
 
1341
        return TreeFileCreate(tree, file_id)
 
1342
    elif kind in ("directory", "root_directory"):
 
1343
        return dir_create
 
1344
    elif kind == "symlink":
 
1345
        return SymlinkCreate(tree.get_symlink_target(file_id))
 
1346
    else:
 
1347
        raise UnsupportedFiletype(kind, tree.id2path(file_id))
1458
1348
 
1459
1349
 
1460
1350
def full_path(entry, tree):
1461
 
    return os.path.join(tree.root, entry.path)
 
1351
    return pathjoin(tree.basedir, entry.path)
 
1352
 
1462
1353
 
1463
1354
def new_delete_entry(entry, tree, inventory, delete):
1464
1355
    if entry.path == "":
1476
1367
    status = os.lstat(full_path)
1477
1368
    if stat.S_ISDIR(file_stat.st_mode):
1478
1369
        action = dir_create
1479
 
    
1480
 
 
1481
 
 
1482
 
        
 
1370
 
 
1371
 
1483
1372
# XXX: Can't we unify this with the regular inventory object
1484
1373
class Inventory(object):
1485
1374
    def __init__(self, inventory):
1514
1403
            return None
1515
1404
        directory = self.get_dir(id)
1516
1405
        if directory == '.':
1517
 
            directory = './.'
 
1406
            directory = u'./.'
1518
1407
        if directory is None:
1519
1408
            return NULL_ID
1520
1409
        return self.get_rinventory().get(directory)
 
1410
 
 
1411