/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/_changeset.py

  • Committer: John Arbash Meinel
  • Date: 2006-02-15 15:18:44 UTC
  • mto: (1185.79.1 bzr-jam-pending)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: john@arbash-meinel.com-20060215151844-ce3e3efccd19da3f
Reverting gpg changes, should not be mainline, see gpg_uses_tempfile plugin.

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