~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Robert Collins
  • Date: 2006-01-05 22:30:59 UTC
  • mto: (1534.1.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1536.
  • Revision ID: robertc@robertcollins.net-20060105223059-a8b64f7b47cf12fb
 * bzrlib.osutils.safe_unicode now exists to provide parameter coercion
   for functions that need unicode strings. (Robert Collins)

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
 
 
22
 
# XXX: mbp: I'm not totally convinced that we should handle conflicts
23
 
# as part of changeset application, rather than only in the merge
24
 
# operation.
25
 
 
26
 
"""Represent and apply a changeset
27
 
 
28
 
Conflicts in applying a changeset are represented as exceptions.
29
 
"""
 
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
 
33
import bzrlib
 
34
from bzrlib.errors import BzrCheckError
30
35
 
31
36
__docformat__ = "restructuredtext"
32
37
 
 
38
 
33
39
NULL_ID = "!NULL"
34
40
 
 
41
 
35
42
class OldFailedTreeOp(Exception):
36
43
    def __init__(self):
37
44
        Exception.__init__(self, "bzr-tree-change contains files from a"
38
45
                           " previous failed merge operation.")
 
46
 
 
47
 
39
48
def invert_dict(dict):
40
49
    newdict = {}
41
50
    for (key,value) in dict.iteritems():
42
51
        newdict[value] = key
43
52
    return newdict
44
53
 
45
 
 
46
54
       
47
 
class ChangeUnixPermissions(object):
 
55
class ChangeExecFlag(object):
48
56
    """This is two-way change, suitable for file modification, creation,
49
57
    deletion"""
50
 
    def __init__(self, old_mode, new_mode):
51
 
        self.old_mode = old_mode
52
 
        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
53
61
 
54
 
    def apply(self, filename, conflict_handler, reverse=False):
55
 
        if not reverse:
56
 
            from_mode = self.old_mode
57
 
            to_mode = self.new_mode
58
 
        else:
59
 
            from_mode = self.new_mode
60
 
            to_mode = self.old_mode
 
62
    def apply(self, filename, conflict_handler):
 
63
        from_exec_flag = self.old_exec_flag
 
64
        to_exec_flag = self.new_exec_flag
61
65
        try:
62
 
            current_mode = os.stat(filename).st_mode &0777
 
66
            current_exec_flag = bool(os.stat(filename).st_mode & 0111)
63
67
        except OSError, e:
64
68
            if e.errno == errno.ENOENT:
65
 
                if conflict_handler.missing_for_chmod(filename) == "skip":
 
69
                if conflict_handler.missing_for_exec_flag(filename) == "skip":
66
70
                    return
67
71
                else:
68
 
                    current_mode = from_mode
 
72
                    current_exec_flag = from_exec_flag
69
73
 
70
 
        if from_mode is not None and current_mode != from_mode:
71
 
            if conflict_handler.wrong_old_perms(filename, from_mode, 
72
 
                                                current_mode) != "continue":
 
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":
73
77
                return
74
78
 
75
 
        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
76
92
            try:
77
93
                os.chmod(filename, to_mode)
78
94
            except IOError, e:
79
95
                if e.errno == errno.ENOENT:
80
 
                    conflict_handler.missing_for_chmod(filename)
 
96
                    conflict_handler.missing_for_exec_flag(filename)
81
97
 
82
98
    def __eq__(self, other):
83
 
        if not isinstance(other, ChangeUnixPermissions):
84
 
            return False
85
 
        elif self.old_mode != other.old_mode:
86
 
            return False
87
 
        elif self.new_mode != other.new_mode:
88
 
            return False
89
 
        else:
90
 
            return True
 
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)
91
102
 
92
103
    def __ne__(self, other):
93
104
        return not (self == other)
94
105
 
95
 
def dir_create(filename, conflict_handler, reverse):
 
106
 
 
107
def dir_create(filename, conflict_handler, reverse=False):
96
108
    """Creates the directory, or deletes it if reverse is true.  Intended to be
97
109
    used with ReplaceContents.
98
110
 
117
129
        try:
118
130
            os.rmdir(filename)
119
131
        except OSError, e:
120
 
            if e.errno != 39:
 
132
            if e.errno != errno.ENOTEMPTY:
121
133
                raise
122
134
            if conflict_handler.rmdir_non_empty(filename) == "skip":
123
135
                return
124
136
            os.rmdir(filename)
125
137
 
126
 
                
127
 
            
128
138
 
129
139
class SymlinkCreate(object):
130
140
    """Creates or deletes a symlink (for use with ReplaceContents)"""
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-")
 
405
        try:
 
406
            new_file = filename+".new"
 
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
 
            os.rename(new_file, filename)
357
 
            return
358
 
        else:
359
 
            assert(status == 1)
360
 
            def get_lines(filename):
361
 
                my_file = file(base, "rb")
362
 
                lines = my_file.readlines()
363
 
                my_file.close()
364
 
            base_lines = get_lines(base)
365
 
            other_lines = get_lines(other)
366
 
            conflict_handler.merge_conflict(new_file, filename, base_lines, 
367
 
                                            other_lines)
 
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)
368
429
 
369
430
 
370
431
def CreateDir():
375
436
    """
376
437
    return ReplaceContents(None, dir_create)
377
438
 
 
439
 
378
440
def DeleteDir():
379
441
    """Convenience function to delete a directory.
380
442
 
383
445
    """
384
446
    return ReplaceContents(dir_create, None)
385
447
 
 
448
 
386
449
def CreateFile(contents):
387
450
    """Convenience fucntion to create a file.
388
451
    
393
456
    """
394
457
    return ReplaceContents(None, FileCreate(contents))
395
458
 
 
459
 
396
460
def DeleteFile(contents):
397
461
    """Convenience fucntion to delete a file.
398
462
    
403
467
    """
404
468
    return ReplaceContents(FileCreate(contents), None)
405
469
 
406
 
def ReplaceFileContents(old_contents, new_contents):
 
470
 
 
471
def ReplaceFileContents(old_tree, new_tree, file_id):
407
472
    """Convenience fucntion to replace the contents of a file.
408
473
    
409
474
    :param old_contents: The contents of the file to replace 
413
478
    :return: A ReplaceContents that will replace the contents of a file a file 
414
479
    :rtype: `ReplaceContents`
415
480
    """
416
 
    return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
 
481
    return ReplaceContents(TreeFileCreate(old_tree, file_id), 
 
482
                           TreeFileCreate(new_tree, file_id))
 
483
 
417
484
 
418
485
def CreateSymlink(target):
419
486
    """Convenience fucntion to create a symlink.
425
492
    """
426
493
    return ReplaceContents(None, SymlinkCreate(target))
427
494
 
 
495
 
428
496
def DeleteSymlink(target):
429
497
    """Convenience fucntion to delete a symlink.
430
498
    
435
503
    """
436
504
    return ReplaceContents(SymlinkCreate(target), None)
437
505
 
 
506
 
438
507
def ChangeTarget(old_target, new_target):
439
508
    """Convenience fucntion to change the target of a symlink.
440
509
    
478
547
        msg = 'Child of !NULL is named "%s", not "./.".' % name
479
548
        InvalidEntry.__init__(self, entry, msg)
480
549
 
 
550
 
481
551
class NullIDAssigned(InvalidEntry):
482
552
    """The id !NULL was assigned to a real entry"""
483
553
    def __init__(self, entry):
489
559
        msg = '"!NULL" id assigned to a file "%s".' % entry.path
490
560
        InvalidEntry.__init__(self, entry, msg)
491
561
 
 
562
 
492
563
class ParentIDIsSelf(InvalidEntry):
493
564
    """An entry is marked as its own parent"""
494
565
    def __init__(self, entry):
501
572
            (entry.path, entry.id)
502
573
        InvalidEntry.__init__(self, entry, msg)
503
574
 
 
575
 
504
576
class ChangesetEntry(object):
505
577
    """An entry the changeset"""
506
578
    def __init__(self, id, parent, path):
525
597
        if self.id  == self.parent:
526
598
            raise ParentIDIsSelf(self)
527
599
 
528
 
    def __str__(self):
 
600
    def __repr__(self):
529
601
        return "ChangesetEntry(%s)" % self.id
530
602
 
 
603
    __str__ = __repr__
 
604
 
531
605
    def __get_dir(self):
532
606
        if self.path is None:
533
607
            return None
534
608
        return os.path.dirname(self.path)
535
609
 
536
610
    def __set_dir(self, dir):
537
 
        self.path = os.path.join(dir, os.path.basename(self.path))
 
611
        self.path = pathjoin(dir, os.path.basename(self.path))
538
612
 
539
613
    dir = property(__get_dir, __set_dir)
540
614
    
544
618
        return os.path.basename(self.path)
545
619
 
546
620
    def __set_name(self, name):
547
 
        self.path = os.path.join(os.path.dirname(self.path), name)
 
621
        self.path = pathjoin(os.path.dirname(self.path), name)
548
622
 
549
623
    name = property(__get_name, __set_name)
550
624
 
554
628
        return os.path.dirname(self.new_path)
555
629
 
556
630
    def __set_new_dir(self, dir):
557
 
        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))
558
632
 
559
633
    new_dir = property(__get_new_dir, __set_new_dir)
560
634
 
564
638
        return os.path.basename(self.new_path)
565
639
 
566
640
    def __set_new_name(self, name):
567
 
        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)
568
642
 
569
643
    new_name = property(__get_new_name, __set_new_name)
570
644
 
576
650
 
577
651
        return (self.parent != self.new_parent or self.name != self.new_name)
578
652
 
579
 
    def is_deletion(self, reverse):
 
653
    def is_deletion(self, reverse=False):
580
654
        """Return true if applying the entry would delete a file/directory.
581
655
 
582
656
        :param reverse: if true, the changeset is being applied in reverse
583
657
        :rtype: bool
584
658
        """
585
 
        return ((self.new_parent is None and not reverse) or 
586
 
                (self.parent is None and reverse))
 
659
        return self.is_creation(not reverse)
587
660
 
588
 
    def is_creation(self, reverse):
 
661
    def is_creation(self, reverse=False):
589
662
        """Return true if applying the entry would create a file/directory.
590
663
 
591
664
        :param reverse: if true, the changeset is being applied in reverse
592
665
        :rtype: bool
593
666
        """
594
 
        return ((self.parent is None and not reverse) or 
595
 
                (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()
596
673
 
597
674
    def is_creation_or_deletion(self):
598
675
        """Return true if applying the entry would create or delete a 
600
677
 
601
678
        :rtype: bool
602
679
        """
603
 
        return self.parent is None or self.new_parent is None
 
680
        return self.is_creation() or self.is_deletion()
604
681
 
605
682
    def get_cset_path(self, mod=False):
606
683
        """Determine the path of the entry according to the changeset.
626
703
                return None
627
704
            return self.path
628
705
 
629
 
    def summarize_name(self, reverse=False):
 
706
    def summarize_name(self):
630
707
        """Produce a one-line summary of the filename.  Indicates renames as
631
708
        old => new, indicates creation as None => new, indicates deletion as
632
709
        old => None.
633
710
 
634
 
        :param changeset: The changeset to get paths from
635
 
        :type changeset: `Changeset`
636
 
        :param reverse: If true, reverse the names in the output
637
 
        :type reverse: bool
638
711
        :rtype: str
639
712
        """
640
713
        orig_path = self.get_cset_path(False)
641
714
        mod_path = self.get_cset_path(True)
642
 
        if orig_path is not None:
 
715
        if orig_path and orig_path.startswith('./'):
643
716
            orig_path = orig_path[2:]
644
 
        if mod_path is not None:
 
717
        if mod_path and mod_path.startswith('./'):
645
718
            mod_path = mod_path[2:]
646
719
        if orig_path == mod_path:
647
720
            return orig_path
648
721
        else:
649
 
            if not reverse:
650
 
                return "%s => %s" % (orig_path, mod_path)
651
 
            else:
652
 
                return "%s => %s" % (mod_path, orig_path)
653
 
 
654
 
 
655
 
    def get_new_path(self, id_map, changeset, reverse=False):
 
722
            return "%s => %s" % (orig_path, mod_path)
 
723
 
 
724
    def get_new_path(self, id_map, changeset):
656
725
        """Determine the full pathname to rename to
657
726
 
658
727
        :param id_map: The map of ids to filenames for the tree
659
728
        :type id_map: Dictionary
660
729
        :param changeset: The changeset to get data from
661
730
        :type changeset: `Changeset`
662
 
        :param reverse: If true, we're applying the changeset in reverse
663
 
        :type reverse: bool
664
731
        :rtype: str
665
732
        """
666
 
        mutter("Finding new path for %s" % self.summarize_name())
667
 
        if reverse:
668
 
            parent = self.parent
669
 
            to_dir = self.dir
670
 
            from_dir = self.new_dir
671
 
            to_name = self.name
672
 
            from_name = self.new_name
673
 
        else:
674
 
            parent = self.new_parent
675
 
            to_dir = self.new_dir
676
 
            from_dir = self.dir
677
 
            to_name = self.new_name
678
 
            from_name = self.name
 
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
679
739
 
680
740
        if to_name is None:
681
741
            return None
682
742
 
683
743
        if parent == NULL_ID or parent is None:
684
 
            if to_name != '.':
 
744
            if to_name != u'.':
685
745
                raise SourceRootHasName(self, to_name)
686
746
            else:
687
 
                return '.'
688
 
        if from_dir == to_dir:
 
747
                return u'.'
 
748
        parent_entry = changeset.entries.get(parent)
 
749
        if parent_entry is None:
689
750
            dir = os.path.dirname(id_map[self.id])
690
751
        else:
691
 
            mutter("path, new_path: %r %r" % (self.path, self.new_path))
692
 
            parent_entry = changeset.entries[parent]
693
 
            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)
694
754
        if from_name == to_name:
695
755
            name = os.path.basename(id_map[self.id])
696
756
        else:
697
757
            name = to_name
698
758
            assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
699
 
        return os.path.join(dir, name)
 
759
        return pathjoin(dir, name)
700
760
 
701
761
    def is_boring(self):
702
762
        """Determines whether the entry does nothing
715
775
        else:
716
776
            return True
717
777
 
718
 
    def apply(self, filename, conflict_handler, reverse=False):
 
778
    def apply(self, filename, conflict_handler):
719
779
        """Applies the file content and/or metadata changes.
720
780
 
721
781
        :param filename: the filename of the entry
722
782
        :type filename: str
723
 
        :param reverse: If true, apply the changes in reverse
724
 
        :type reverse: bool
725
783
        """
726
 
        if self.is_deletion(reverse) and self.metadata_change is not None:
727
 
            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)
728
786
        if self.contents_change is not None:
729
 
            self.contents_change.apply(filename, conflict_handler, reverse)
730
 
        if not self.is_deletion(reverse) and self.metadata_change is not None:
731
 
            self.metadata_change.apply(filename, conflict_handler, reverse)
 
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
 
732
791
 
733
792
class IDPresent(Exception):
734
793
    def __init__(self, id):
737
796
        Exception.__init__(self, msg)
738
797
        self.id = id
739
798
 
 
799
 
740
800
class Changeset(object):
741
801
    """A set of changes to apply"""
742
802
    def __init__(self):
748
808
            raise IDPresent(entry.id)
749
809
        self.entries[entry.id] = entry
750
810
 
751
 
def my_sort(sequence, key, reverse=False):
752
 
    """A sort function that supports supplying a key for comparison
753
 
    
754
 
    :param sequence: The sequence to sort
755
 
    :param key: A callable object that returns the values to be compared
756
 
    :param reverse: If true, sort in reverse order
757
 
    :type reverse: bool
758
 
    """
759
 
    def cmp_by_key(entry_a, entry_b):
760
 
        if reverse:
761
 
            tmp=entry_a
762
 
            entry_a = entry_b
763
 
            entry_b = tmp
764
 
        return cmp(key(entry_a), key(entry_b))
765
 
    sequence.sort(cmp_by_key)
766
811
 
767
 
def get_rename_entries(changeset, inventory, reverse):
 
812
def get_rename_entries(changeset, inventory):
768
813
    """Return a list of entries that will be renamed.  Entries are sorted from
769
814
    longest to shortest source path and from shortest to longest target path.
770
815
 
772
817
    :type changeset: `Changeset`
773
818
    :param inventory: The source of current tree paths for the given ids
774
819
    :type inventory: Dictionary
775
 
    :param reverse: If true, the changeset is being applied in reverse
776
 
    :type reverse: bool
777
820
    :return: source entries and target entries as a tuple
778
821
    :rtype: (List, List)
779
822
    """
780
823
    source_entries = [x for x in changeset.entries.itervalues() 
781
 
                      if x.needs_rename()]
 
824
                      if x.needs_rename() or x.is_creation_or_deletion()]
782
825
    # these are done from longest path to shortest, to avoid deleting a
783
826
    # parent before its children are deleted/renamed 
784
827
    def longest_to_shortest(entry):
787
830
            return 0
788
831
        else:
789
832
            return len(path)
790
 
    my_sort(source_entries, longest_to_shortest, reverse=True)
 
833
    source_entries.sort(None, longest_to_shortest, True)
791
834
 
792
835
    target_entries = source_entries[:]
793
836
    # These are done from shortest to longest path, to avoid creating a
794
837
    # child before its parent has been created/renamed
795
838
    def shortest_to_longest(entry):
796
 
        path = entry.get_new_path(inventory, changeset, reverse)
 
839
        path = entry.get_new_path(inventory, changeset)
797
840
        if path is None:
798
841
            return 0
799
842
        else:
800
843
            return len(path)
801
 
    my_sort(target_entries, shortest_to_longest)
 
844
    target_entries.sort(None, shortest_to_longest)
802
845
    return (source_entries, target_entries)
803
846
 
 
847
 
804
848
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir, 
805
 
                          conflict_handler, reverse):
 
849
                          conflict_handler):
806
850
    """Delete and rename entries as appropriate.  Entries are renamed to temp
807
851
    names.  A map of id -> temp name (or None, for deletions) is returned.
808
852
 
812
856
    :type inventory: Dictionary
813
857
    :param dir: The directory to apply changes to
814
858
    :type dir: str
815
 
    :param reverse: Apply changes in reverse
816
 
    :type reverse: bool
817
859
    :return: a mapping of id to temporary name
818
860
    :rtype: Dictionary
819
861
    """
820
862
    temp_name = {}
821
863
    for i in range(len(source_entries)):
822
864
        entry = source_entries[i]
823
 
        if entry.is_deletion(reverse):
824
 
            path = os.path.join(dir, inventory[entry.id])
825
 
            entry.apply(path, conflict_handler, reverse)
 
865
        if entry.is_deletion():
 
866
            path = pathjoin(dir, inventory[entry.id])
 
867
            entry.apply(path, conflict_handler)
826
868
            temp_name[entry.id] = None
827
869
 
828
 
        else:
829
 
            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))
830
874
            src_path = inventory.get(entry.id)
831
875
            if src_path is not None:
832
 
                src_path = os.path.join(dir, src_path)
 
876
                src_path = pathjoin(dir, src_path)
833
877
                try:
834
 
                    os.rename(src_path, to_name)
 
878
                    rename(src_path, to_name)
835
879
                    temp_name[entry.id] = to_name
836
880
                except OSError, e:
837
881
                    if e.errno != errno.ENOENT:
838
882
                        raise
839
 
                    if conflict_handler.missing_for_rename(src_path) == "skip":
 
883
                    if conflict_handler.missing_for_rename(src_path, to_name) \
 
884
                        == "skip":
840
885
                        continue
841
886
 
842
887
    return temp_name
843
888
 
844
889
 
845
890
def rename_to_new_create(changed_inventory, target_entries, inventory, 
846
 
                         changeset, dir, conflict_handler, reverse):
 
891
                         changeset, dir, conflict_handler):
847
892
    """Rename entries with temp names to their final names, create new files.
848
893
 
849
894
    :param changed_inventory: A mapping of id to temporary name
854
899
    :type changeset: `Changeset`
855
900
    :param dir: The directory to apply changes to
856
901
    :type dir: str
857
 
    :param reverse: If true, apply changes in reverse
858
 
    :type reverse: bool
859
902
    """
860
903
    for entry in target_entries:
861
 
        new_tree_path = entry.get_new_path(inventory, changeset, reverse)
 
904
        new_tree_path = entry.get_new_path(inventory, changeset)
862
905
        if new_tree_path is None:
863
906
            continue
864
 
        new_path = os.path.join(dir, new_tree_path)
 
907
        new_path = pathjoin(dir, new_tree_path)
865
908
        old_path = changed_inventory.get(entry.id)
866
 
        if os.path.exists(new_path):
 
909
        if bzrlib.osutils.lexists(new_path):
867
910
            if conflict_handler.target_exists(entry, new_path, old_path) == \
868
911
                "skip":
869
912
                continue
870
 
        if entry.is_creation(reverse):
871
 
            entry.apply(new_path, conflict_handler, reverse)
 
913
        if entry.is_creation():
 
914
            entry.apply(new_path, conflict_handler)
872
915
            changed_inventory[entry.id] = new_tree_path
873
 
        else:
 
916
        elif entry.needs_rename():
 
917
            if entry.is_deletion():
 
918
                continue
874
919
            if old_path is None:
875
920
                continue
876
921
            try:
877
 
                os.rename(old_path, new_path)
 
922
                mutter('rename %s to final name %s', old_path, new_path)
 
923
                rename(old_path, new_path)
878
924
                changed_inventory[entry.id] = new_tree_path
879
925
            except OSError, e:
880
 
                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
 
881
929
 
882
930
class TargetExists(Exception):
883
931
    def __init__(self, entry, target):
886
934
        self.entry = entry
887
935
        self.target = target
888
936
 
 
937
 
889
938
class RenameConflict(Exception):
890
939
    def __init__(self, id, this_name, base_name, other_name):
891
940
        msg = """Trees all have different names for a file
898
947
        self.base_name = base_name
899
948
        self_other_name = other_name
900
949
 
 
950
 
901
951
class MoveConflict(Exception):
902
952
    def __init__(self, id, this_parent, base_parent, other_parent):
903
953
        msg = """The file is in different directories in every tree
910
960
        self.base_parent = base_parent
911
961
        self_other_parent = other_parent
912
962
 
 
963
 
913
964
class MergeConflict(Exception):
914
965
    def __init__(self, this_path):
915
966
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
916
967
        self.this_path = this_path
917
968
 
918
 
class MergePermissionConflict(Exception):
919
 
    def __init__(self, this_path, base_path, other_path):
920
 
        this_perms = os.stat(this_path).st_mode & 0755
921
 
        base_perms = os.stat(base_path).st_mode & 0755
922
 
        other_perms = os.stat(other_path).st_mode & 0755
923
 
        msg = """Conflicting permission for %s
924
 
this: %o
925
 
base: %o
926
 
other: %o
927
 
        """ % (this_path, this_perms, base_perms, other_perms)
928
 
        self.this_path = this_path
929
 
        self.base_path = base_path
930
 
        self.other_path = other_path
931
 
        Exception.__init__(self, msg)
932
969
 
933
970
class WrongOldContents(Exception):
934
971
    def __init__(self, filename):
936
973
        self.filename = filename
937
974
        Exception.__init__(self, msg)
938
975
 
939
 
class WrongOldPermissions(Exception):
940
 
    def __init__(self, filename, old_perms, new_perms):
941
 
        msg = "Permission missmatch on %s:\n" \
942
 
        "Expected 0%o, got 0%o." % (filename, old_perms, new_perms)
 
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)
943
981
        self.filename = filename
944
982
        Exception.__init__(self, msg)
945
983
 
 
984
 
946
985
class RemoveContentsConflict(Exception):
947
986
    def __init__(self, filename):
948
987
        msg = "Conflict deleting %s, which has different contents in BASE"\
950
989
        self.filename = filename
951
990
        Exception.__init__(self, msg)
952
991
 
 
992
 
953
993
class DeletingNonEmptyDirectory(Exception):
954
994
    def __init__(self, filename):
955
995
        msg = "Trying to remove dir %s while it still had files" % filename
963
1003
        Exception.__init__(self, msg)
964
1004
        self.filename = filename
965
1005
 
966
 
class MissingPermsFile(Exception):
 
1006
 
 
1007
class MissingForSetExec(Exception):
967
1008
    def __init__(self, filename):
968
1009
        msg = "Attempt to change permissions on  %s, which does not exist" %\
969
1010
            filename
970
1011
        Exception.__init__(self, msg)
971
1012
        self.filename = filename
972
1013
 
 
1014
 
973
1015
class MissingForRm(Exception):
974
1016
    def __init__(self, filename):
975
1017
        msg = "Attempt to remove missing path %s" % filename
978
1020
 
979
1021
 
980
1022
class MissingForRename(Exception):
981
 
    def __init__(self, filename):
982
 
        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)
983
1025
        Exception.__init__(self, msg)
984
1026
        self.filename = filename
985
1027
 
 
1028
 
986
1029
class NewContentsConflict(Exception):
987
1030
    def __init__(self, filename):
988
1031
        msg = "Conflicting contents for new file %s" % (filename)
989
1032
        Exception.__init__(self, msg)
990
1033
 
991
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
 
992
1047
class MissingForMerge(Exception):
993
1048
    def __init__(self, filename):
994
1049
        msg = "The file %s was modified, but does not exist in this tree"\
1003
1058
    descend from this class if they have a better way to handle some or
1004
1059
    all types of conflict.
1005
1060
    """
1006
 
    def __init__(self, dir):
1007
 
        self.dir = dir
1008
 
    
1009
1061
    def missing_parent(self, pathname):
1010
1062
        parent = os.path.dirname(pathname)
1011
1063
        raise Exception("Parent directory missing for %s" % pathname)
1029
1081
        os.unlink(new_file)
1030
1082
        raise MergeConflict(this_path)
1031
1083
 
1032
 
    def permission_conflict(self, this_path, base_path, other_path):
1033
 
        raise MergePermissionConflict(this_path, base_path, other_path)
1034
 
 
1035
1084
    def wrong_old_contents(self, filename, expected_contents):
1036
1085
        raise WrongOldContents(filename)
1037
1086
 
1038
1087
    def rem_contents_conflict(self, filename, this_contents, base_contents):
1039
1088
        raise RemoveContentsConflict(filename)
1040
1089
 
1041
 
    def wrong_old_perms(self, filename, old_perms, new_perms):
1042
 
        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)
1043
1092
 
1044
1093
    def rmdir_non_empty(self, filename):
1045
1094
        raise DeletingNonEmptyDirectory(filename)
1050
1099
    def patch_target_missing(self, filename, contents):
1051
1100
        raise PatchTargetMissing(filename)
1052
1101
 
1053
 
    def missing_for_chmod(self, filename):
1054
 
        raise MissingPermsFile(filename)
 
1102
    def missing_for_exec_flag(self, filename):
 
1103
        raise MissingForExecFlag(filename)
1055
1104
 
1056
1105
    def missing_for_rm(self, filename, change):
1057
1106
        raise MissingForRm(filename)
1058
1107
 
1059
 
    def missing_for_rename(self, filename):
1060
 
        raise MissingForRename(filename)
 
1108
    def missing_for_rename(self, filename, to_path):
 
1109
        raise MissingForRename(filename, to_path)
1061
1110
 
1062
1111
    def missing_for_merge(self, file_id, other_path):
1063
1112
        raise MissingForMerge(other_path)
1065
1114
    def new_contents_conflict(self, filename, other_contents):
1066
1115
        raise NewContentsConflict(filename)
1067
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
 
1068
1124
    def finalize(self):
1069
1125
        pass
1070
1126
 
1071
 
def apply_changeset(changeset, inventory, dir, conflict_handler=None, 
1072
 
                    reverse=False):
 
1127
 
 
1128
def apply_changeset(changeset, inventory, dir, conflict_handler=None):
1073
1129
    """Apply a changeset to a directory.
1074
1130
 
1075
1131
    :param changeset: The changes to perform
1078
1134
    :type inventory: Dictionary
1079
1135
    :param dir: The path of the directory to apply the changes to
1080
1136
    :type dir: str
1081
 
    :param reverse: If true, apply the changes in reverse
1082
 
    :type reverse: bool
1083
1137
    :return: The mapping of the changed entries
1084
1138
    :rtype: Dictionary
1085
1139
    """
1086
1140
    if conflict_handler is None:
1087
 
        conflict_handler = ExceptionConflictHandler(dir)
1088
 
    temp_dir = os.path.join(dir, "bzr-tree-change")
 
1141
        conflict_handler = ExceptionConflictHandler()
 
1142
    temp_dir = pathjoin(dir, "bzr-tree-change")
1089
1143
    try:
1090
1144
        os.mkdir(temp_dir)
1091
1145
    except OSError, e:
1101
1155
    
1102
1156
    #apply changes that don't affect filenames
1103
1157
    for entry in changeset.entries.itervalues():
1104
 
        if not entry.is_creation_or_deletion():
1105
 
            path = os.path.join(dir, inventory[entry.id])
1106
 
            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)
1107
1165
 
1108
1166
    # Apply renames in stages, to minimize conflicts:
1109
1167
    # Only files whose name or parent change are interesting, because their
1110
1168
    # target name may exist in the source tree.  If a directory's name changes,
1111
1169
    # that doesn't make its children interesting.
1112
 
    (source_entries, target_entries) = get_rename_entries(changeset, inventory,
1113
 
                                                          reverse)
 
1170
    (source_entries, target_entries) = get_rename_entries(changeset, inventory)
1114
1171
 
1115
1172
    changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
1116
 
                                              temp_dir, conflict_handler,
1117
 
                                              reverse)
 
1173
                                              temp_dir, conflict_handler)
1118
1174
 
1119
1175
    rename_to_new_create(changed_inventory, target_entries, inventory,
1120
 
                         changeset, dir, conflict_handler, reverse)
 
1176
                         changeset, dir, conflict_handler)
1121
1177
    os.rmdir(temp_dir)
1122
1178
    return changed_inventory
1123
1179
 
1124
1180
 
1125
 
def apply_changeset_tree(cset, tree, reverse=False):
 
1181
def apply_changeset_tree(cset, tree):
1126
1182
    r_inventory = {}
1127
1183
    for entry in tree.source_inventory().itervalues():
1128
1184
        inventory[entry.id] = entry.path
1129
 
    new_inventory = apply_changeset(cset, r_inventory, tree.root,
1130
 
                                    reverse=reverse)
 
1185
    new_inventory = apply_changeset(cset, r_inventory, tree.basedir)
1131
1186
    new_entries, remove_entries = \
1132
 
        get_inventory_change(inventory, new_inventory, cset, reverse)
 
1187
        get_inventory_change(inventory, new_inventory, cset)
1133
1188
    tree.update_source_inventory(new_entries, remove_entries)
1134
1189
 
1135
1190
 
1136
 
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
 
1191
def get_inventory_change(inventory, new_inventory, cset):
1137
1192
    new_entries = {}
1138
1193
    remove_entries = []
1139
1194
    for entry in cset.entries.itervalues():
1158
1213
        print entry.id
1159
1214
        print entry.summarize_name(cset)
1160
1215
 
1161
 
class CompositionFailure(Exception):
1162
 
    def __init__(self, old_entry, new_entry, problem):
1163
 
        msg = "Unable to conpose entries.\n %s" % problem
1164
 
        Exception.__init__(self, msg)
1165
 
 
1166
 
class IDMismatch(CompositionFailure):
1167
 
    def __init__(self, old_entry, new_entry):
1168
 
        problem = "Attempt to compose entries with different ids: %s and %s" %\
1169
 
            (old_entry.id, new_entry.id)
1170
 
        CompositionFailure.__init__(self, old_entry, new_entry, problem)
1171
 
 
1172
 
def compose_changesets(old_cset, new_cset):
1173
 
    """Combine two changesets into one.  This works well for exact patching.
1174
 
    Otherwise, not so well.
1175
 
 
1176
 
    :param old_cset: The first changeset that would be applied
1177
 
    :type old_cset: `Changeset`
1178
 
    :param new_cset: The second changeset that would be applied
1179
 
    :type new_cset: `Changeset`
1180
 
    :return: A changeset that combines the changes in both changesets
1181
 
    :rtype: `Changeset`
1182
 
    """
1183
 
    composed = Changeset()
1184
 
    for old_entry in old_cset.entries.itervalues():
1185
 
        new_entry = new_cset.entries.get(old_entry.id)
1186
 
        if new_entry is None:
1187
 
            composed.add_entry(old_entry)
1188
 
        else:
1189
 
            composed_entry = compose_entries(old_entry, new_entry)
1190
 
            if composed_entry.parent is not None or\
1191
 
                composed_entry.new_parent is not None:
1192
 
                composed.add_entry(composed_entry)
1193
 
    for new_entry in new_cset.entries.itervalues():
1194
 
        if not old_cset.entries.has_key(new_entry.id):
1195
 
            composed.add_entry(new_entry)
1196
 
    return composed
1197
 
 
1198
 
def compose_entries(old_entry, new_entry):
1199
 
    """Combine two entries into one.
1200
 
 
1201
 
    :param old_entry: The first entry that would be applied
1202
 
    :type old_entry: ChangesetEntry
1203
 
    :param old_entry: The second entry that would be applied
1204
 
    :type old_entry: ChangesetEntry
1205
 
    :return: A changeset entry combining both entries
1206
 
    :rtype: `ChangesetEntry`
1207
 
    """
1208
 
    if old_entry.id != new_entry.id:
1209
 
        raise IDMismatch(old_entry, new_entry)
1210
 
    output = ChangesetEntry(old_entry.id, old_entry.parent, old_entry.path)
1211
 
 
1212
 
    if (old_entry.parent != old_entry.new_parent or 
1213
 
        new_entry.parent != new_entry.new_parent):
1214
 
        output.new_parent = new_entry.new_parent
1215
 
 
1216
 
    if (old_entry.path != old_entry.new_path or 
1217
 
        new_entry.path != new_entry.new_path):
1218
 
        output.new_path = new_entry.new_path
1219
 
 
1220
 
    output.contents_change = compose_contents(old_entry, new_entry)
1221
 
    output.metadata_change = compose_metadata(old_entry, new_entry)
1222
 
    return output
1223
 
 
1224
 
def compose_contents(old_entry, new_entry):
1225
 
    """Combine the contents of two changeset entries.  Entries are combined
1226
 
    intelligently where possible, but the fallback behavior returns an 
1227
 
    ApplySequence.
1228
 
 
1229
 
    :param old_entry: The first entry that would be applied
1230
 
    :type old_entry: `ChangesetEntry`
1231
 
    :param new_entry: The second entry that would be applied
1232
 
    :type new_entry: `ChangesetEntry`
1233
 
    :return: A combined contents change
1234
 
    :rtype: anything supporting the apply(reverse=False) method
1235
 
    """
1236
 
    old_contents = old_entry.contents_change
1237
 
    new_contents = new_entry.contents_change
1238
 
    if old_entry.contents_change is None:
1239
 
        return new_entry.contents_change
1240
 
    elif new_entry.contents_change is None:
1241
 
        return old_entry.contents_change
1242
 
    elif isinstance(old_contents, ReplaceContents) and \
1243
 
        isinstance(new_contents, ReplaceContents):
1244
 
        if old_contents.old_contents == new_contents.new_contents:
1245
 
            return None
1246
 
        else:
1247
 
            return ReplaceContents(old_contents.old_contents,
1248
 
                                   new_contents.new_contents)
1249
 
    elif isinstance(old_contents, ApplySequence):
1250
 
        output = ApplySequence(old_contents.changes)
1251
 
        if isinstance(new_contents, ApplySequence):
1252
 
            output.changes.extend(new_contents.changes)
1253
 
        else:
1254
 
            output.changes.append(new_contents)
1255
 
        return output
1256
 
    elif isinstance(new_contents, ApplySequence):
1257
 
        output = ApplySequence((old_contents.changes,))
1258
 
        output.extend(new_contents.changes)
1259
 
        return output
1260
 
    else:
1261
 
        return ApplySequence((old_contents, new_contents))
1262
 
 
1263
 
def compose_metadata(old_entry, new_entry):
1264
 
    old_meta = old_entry.metadata_change
1265
 
    new_meta = new_entry.metadata_change
1266
 
    if old_meta is None:
1267
 
        return new_meta
1268
 
    elif new_meta is None:
1269
 
        return old_meta
1270
 
    elif isinstance(old_meta, ChangeUnixPermissions) and \
1271
 
        isinstance(new_meta, ChangeUnixPermissions):
1272
 
        return ChangeUnixPermissions(old_meta.old_mode, new_meta.new_mode)
1273
 
    else:
1274
 
        return ApplySequence(old_meta, new_meta)
1275
 
 
1276
 
 
1277
 
def changeset_is_null(changeset):
1278
 
    for entry in changeset.entries.itervalues():
1279
 
        if not entry.is_boring():
1280
 
            return False
1281
 
    return True
1282
 
 
1283
 
class UnsuppportedFiletype(Exception):
1284
 
    def __init__(self, full_path, stat_result):
1285
 
        msg = "The file \"%s\" is not a supported filetype." % full_path
 
1216
 
 
1217
class UnsupportedFiletype(Exception):
 
1218
    def __init__(self, kind, full_path):
 
1219
        msg = "The file \"%s\" is a %s, which is not a supported filetype." \
 
1220
            % (full_path, kind)
1286
1221
        Exception.__init__(self, msg)
1287
1222
        self.full_path = full_path
1288
 
        self.stat_result = stat_result
 
1223
        self.kind = kind
 
1224
 
1289
1225
 
1290
1226
def generate_changeset(tree_a, tree_b, interesting_ids=None):
1291
1227
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
1292
1228
 
 
1229
 
1293
1230
class ChangesetGenerator(object):
1294
1231
    def __init__(self, tree_a, tree_b, interesting_ids=None):
1295
1232
        object.__init__(self)
1331
1268
    def get_entry(self, file_id, tree):
1332
1269
        if not tree.has_or_had_id(file_id):
1333
1270
            return None
1334
 
        return tree.tree.inventory[file_id]
 
1271
        return tree.inventory[file_id]
1335
1272
 
1336
1273
    def get_entry_parent(self, entry):
1337
1274
        if entry is None:
1380
1317
            return self.make_entry(id, only_interesting=False)
1381
1318
        else:
1382
1319
            return cs_entry
1383
 
        
1384
1320
 
1385
1321
    def make_entry(self, id, only_interesting=True):
1386
1322
        cs_entry = self.make_basic_entry(id, only_interesting)
1387
1323
 
1388
1324
        if cs_entry is None:
1389
1325
            return None
 
1326
 
 
1327
        cs_entry.metadata_change = self.make_exec_flag_change(id)
 
1328
 
1390
1329
        if id in self.tree_a and id in self.tree_b:
1391
1330
            a_sha1 = self.tree_a.get_file_sha1(id)
1392
1331
            b_sha1 = self.tree_b.get_file_sha1(id)
1393
1332
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
1394
1333
                return cs_entry
1395
1334
 
1396
 
        full_path_a = self.tree_a.readonly_path(id)
1397
 
        full_path_b = self.tree_b.readonly_path(id)
1398
 
        stat_a = self.lstat(full_path_a)
1399
 
        stat_b = self.lstat(full_path_b)
1400
 
        
1401
 
        cs_entry.metadata_change = self.make_mode_change(stat_a, stat_b)
1402
 
        cs_entry.contents_change = self.make_contents_change(full_path_a,
1403
 
                                                             stat_a, 
1404
 
                                                             full_path_b, 
1405
 
                                                             stat_b)
 
1335
        cs_entry.contents_change = self.make_contents_change(id)
1406
1336
        return cs_entry
1407
1337
 
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)
 
1338
    def make_exec_flag_change(self, file_id):
 
1339
        exec_flag_a = exec_flag_b = None
 
1340
        if file_id in self.tree_a and self.tree_a.kind(file_id) == "file":
 
1341
            exec_flag_a = self.tree_a.is_executable(file_id)
 
1342
 
 
1343
        if file_id in self.tree_b and self.tree_b.kind(file_id) == "file":
 
1344
            exec_flag_b = self.tree_b.is_executable(file_id)
 
1345
 
 
1346
        if exec_flag_a == exec_flag_b:
 
1347
            return None
 
1348
        return ChangeExecFlag(exec_flag_a, exec_flag_b)
 
1349
 
 
1350
    def make_contents_change(self, file_id):
 
1351
        a_contents = get_contents(self.tree_a, file_id)
 
1352
        b_contents = get_contents(self.tree_b, file_id)
1433
1353
        if a_contents == b_contents:
1434
1354
            return None
1435
1355
        return ReplaceContents(a_contents, b_contents)
1436
1356
 
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
1357
 
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
 
1358
def get_contents(tree, file_id):
 
1359
    """Return the appropriate contents to create a copy of file_id from tree"""
 
1360
    if file_id not in tree:
 
1361
        return None
 
1362
    kind = tree.kind(file_id)
 
1363
    if kind == "file":
 
1364
        return TreeFileCreate(tree, file_id)
 
1365
    elif kind in ("directory", "root_directory"):
 
1366
        return dir_create
 
1367
    elif kind == "symlink":
 
1368
        return SymlinkCreate(tree.get_symlink_target(file_id))
 
1369
    else:
 
1370
        raise UnsupportedFiletype(kind, tree.id2path(file_id))
1458
1371
 
1459
1372
 
1460
1373
def full_path(entry, tree):
1461
 
    return os.path.join(tree.root, entry.path)
 
1374
    return pathjoin(tree.basedir, entry.path)
 
1375
 
1462
1376
 
1463
1377
def new_delete_entry(entry, tree, inventory, delete):
1464
1378
    if entry.path == "":
1476
1390
    status = os.lstat(full_path)
1477
1391
    if stat.S_ISDIR(file_stat.st_mode):
1478
1392
        action = dir_create
1479
 
    
1480
 
 
1481
 
 
1482
 
        
 
1393
 
 
1394
 
1483
1395
# XXX: Can't we unify this with the regular inventory object
1484
1396
class Inventory(object):
1485
1397
    def __init__(self, inventory):
1514
1426
            return None
1515
1427
        directory = self.get_dir(id)
1516
1428
        if directory == '.':
1517
 
            directory = './.'
 
1429
            directory = u'./.'
1518
1430
        if directory is None:
1519
1431
            return NULL_ID
1520
1432
        return self.get_rinventory().get(directory)
 
1433
 
 
1434