~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Martin Pool
  • Date: 2005-09-06 02:26:28 UTC
  • Revision ID: mbp@sourcefrog.net-20050906022628-66d65f0feb4a9e80
- implement version 5 xml storage, and tests

  This stores files identified by the version that introduced the 
  text, and the version that introduced the name.  Entry kinds are
  given by the xml tag not an explicit kind field.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004 Aaron Bentley <aaron.bentley@utoronto.ca>
 
2
#
 
3
#    This program is free software; you can redistribute it and/or modify
 
4
#    it under the terms of the GNU General Public License as published by
 
5
#    the Free Software Foundation; either version 2 of the License, or
 
6
#    (at your option) any later version.
 
7
#
 
8
#    This program is distributed in the hope that it will be useful,
 
9
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
#    GNU General Public License for more details.
 
12
#
 
13
#    You should have received a copy of the GNU General Public License
 
14
#    along with this program; if not, write to the Free Software
 
15
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
import os.path
 
17
import errno
 
18
import patch
 
19
import stat
 
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
"""
 
30
 
 
31
__docformat__ = "restructuredtext"
 
32
 
 
33
NULL_ID = "!NULL"
 
34
 
 
35
class OldFailedTreeOp(Exception):
 
36
    def __init__(self):
 
37
        Exception.__init__(self, "bzr-tree-change contains files from a"
 
38
                           " previous failed merge operation.")
 
39
def invert_dict(dict):
 
40
    newdict = {}
 
41
    for (key,value) in dict.iteritems():
 
42
        newdict[value] = key
 
43
    return newdict
 
44
 
 
45
 
 
46
       
 
47
class ChangeUnixPermissions(object):
 
48
    """This is two-way change, suitable for file modification, creation,
 
49
    deletion"""
 
50
    def __init__(self, old_mode, new_mode):
 
51
        self.old_mode = old_mode
 
52
        self.new_mode = new_mode
 
53
 
 
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
 
61
        try:
 
62
            current_mode = os.stat(filename).st_mode &0777
 
63
        except OSError, e:
 
64
            if e.errno == errno.ENOENT:
 
65
                if conflict_handler.missing_for_chmod(filename) == "skip":
 
66
                    return
 
67
                else:
 
68
                    current_mode = from_mode
 
69
 
 
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":
 
73
                return
 
74
 
 
75
        if to_mode is not None:
 
76
            try:
 
77
                os.chmod(filename, to_mode)
 
78
            except IOError, e:
 
79
                if e.errno == errno.ENOENT:
 
80
                    conflict_handler.missing_for_chmod(filename)
 
81
 
 
82
    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
 
91
 
 
92
    def __ne__(self, other):
 
93
        return not (self == other)
 
94
 
 
95
def dir_create(filename, conflict_handler, reverse):
 
96
    """Creates the directory, or deletes it if reverse is true.  Intended to be
 
97
    used with ReplaceContents.
 
98
 
 
99
    :param filename: The name of the directory to create
 
100
    :type filename: str
 
101
    :param reverse: If true, delete the directory, instead
 
102
    :type reverse: bool
 
103
    """
 
104
    if not reverse:
 
105
        try:
 
106
            os.mkdir(filename)
 
107
        except OSError, e:
 
108
            if e.errno != errno.EEXIST:
 
109
                raise
 
110
            if conflict_handler.dir_exists(filename) == "continue":
 
111
                os.mkdir(filename)
 
112
        except IOError, e:
 
113
            if e.errno == errno.ENOENT:
 
114
                if conflict_handler.missing_parent(filename)=="continue":
 
115
                    file(filename, "wb").write(self.contents)
 
116
    else:
 
117
        try:
 
118
            os.rmdir(filename)
 
119
        except OSError, e:
 
120
            if e.errno != 39:
 
121
                raise
 
122
            if conflict_handler.rmdir_non_empty(filename) == "skip":
 
123
                return
 
124
            os.rmdir(filename)
 
125
 
 
126
                
 
127
            
 
128
 
 
129
class SymlinkCreate(object):
 
130
    """Creates or deletes a symlink (for use with ReplaceContents)"""
 
131
    def __init__(self, contents):
 
132
        """Constructor.
 
133
 
 
134
        :param contents: The filename of the target the symlink should point to
 
135
        :type contents: str
 
136
        """
 
137
        self.target = contents
 
138
 
 
139
    def __call__(self, filename, conflict_handler, reverse):
 
140
        """Creates or destroys the symlink.
 
141
 
 
142
        :param filename: The name of the symlink to create
 
143
        :type filename: str
 
144
        """
 
145
        if reverse:
 
146
            assert(os.readlink(filename) == self.target)
 
147
            os.unlink(filename)
 
148
        else:
 
149
            try:
 
150
                os.symlink(self.target, filename)
 
151
            except OSError, e:
 
152
                if e.errno != errno.EEXIST:
 
153
                    raise
 
154
                if conflict_handler.link_name_exists(filename) == "continue":
 
155
                    os.symlink(self.target, filename)
 
156
 
 
157
    def __eq__(self, other):
 
158
        if not isinstance(other, SymlinkCreate):
 
159
            return False
 
160
        elif self.target != other.target:
 
161
            return False
 
162
        else:
 
163
            return True
 
164
 
 
165
    def __ne__(self, other):
 
166
        return not (self == other)
 
167
 
 
168
class FileCreate(object):
 
169
    """Create or delete a file (for use with ReplaceContents)"""
 
170
    def __init__(self, contents):
 
171
        """Constructor
 
172
 
 
173
        :param contents: The contents of the file to write
 
174
        :type contents: str
 
175
        """
 
176
        self.contents = contents
 
177
 
 
178
    def __repr__(self):
 
179
        return "FileCreate(%i b)" % len(self.contents)
 
180
 
 
181
    def __eq__(self, other):
 
182
        if not isinstance(other, FileCreate):
 
183
            return False
 
184
        elif self.contents != other.contents:
 
185
            return False
 
186
        else:
 
187
            return True
 
188
 
 
189
    def __ne__(self, other):
 
190
        return not (self == other)
 
191
 
 
192
    def __call__(self, filename, conflict_handler, reverse):
 
193
        """Create or delete a file
 
194
 
 
195
        :param filename: The name of the file to create
 
196
        :type filename: str
 
197
        :param reverse: Delete the file instead of creating it
 
198
        :type reverse: bool
 
199
        """
 
200
        if not reverse:
 
201
            try:
 
202
                file(filename, "wb").write(self.contents)
 
203
            except IOError, e:
 
204
                if e.errno == errno.ENOENT:
 
205
                    if conflict_handler.missing_parent(filename)=="continue":
 
206
                        file(filename, "wb").write(self.contents)
 
207
                else:
 
208
                    raise
 
209
 
 
210
        else:
 
211
            try:
 
212
                if (file(filename, "rb").read() != self.contents):
 
213
                    direction = conflict_handler.wrong_old_contents(filename,
 
214
                                                                    self.contents)
 
215
                    if  direction != "continue":
 
216
                        return
 
217
                os.unlink(filename)
 
218
            except IOError, e:
 
219
                if e.errno != errno.ENOENT:
 
220
                    raise
 
221
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
 
222
                    return
 
223
 
 
224
                    
 
225
 
 
226
def reversed(sequence):
 
227
    max = len(sequence) - 1
 
228
    for i in range(len(sequence)):
 
229
        yield sequence[max - i]
 
230
 
 
231
class ReplaceContents(object):
 
232
    """A contents-replacement framework.  It allows a file/directory/symlink to
 
233
    be created, deleted, or replaced with another file/directory/symlink.
 
234
    Arguments must be callable with (filename, reverse).
 
235
    """
 
236
    def __init__(self, old_contents, new_contents):
 
237
        """Constructor.
 
238
 
 
239
        :param old_contents: The change to reverse apply (e.g. a deletion), \
 
240
        when going forwards.
 
241
        :type old_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
 
242
        NoneType, etc.
 
243
        :param new_contents: The second change to apply (e.g. a creation), \
 
244
        when going forwards.
 
245
        :type new_contents: `dir_create`, `SymlinkCreate`, `FileCreate`, \
 
246
        NoneType, etc.
 
247
        """
 
248
        self.old_contents=old_contents
 
249
        self.new_contents=new_contents
 
250
 
 
251
    def __repr__(self):
 
252
        return "ReplaceContents(%r -> %r)" % (self.old_contents,
 
253
                                              self.new_contents)
 
254
 
 
255
    def __eq__(self, other):
 
256
        if not isinstance(other, ReplaceContents):
 
257
            return False
 
258
        elif self.old_contents != other.old_contents:
 
259
            return False
 
260
        elif self.new_contents != other.new_contents:
 
261
            return False
 
262
        else:
 
263
            return True
 
264
    def __ne__(self, other):
 
265
        return not (self == other)
 
266
 
 
267
    def apply(self, filename, conflict_handler, reverse=False):
 
268
        """Applies the FileReplacement to the specified filename
 
269
 
 
270
        :param filename: The name of the file to apply changes to
 
271
        :type filename: str
 
272
        :param reverse: If true, apply the change in reverse
 
273
        :type reverse: bool
 
274
        """
 
275
        if not reverse:
 
276
            undo = self.old_contents
 
277
            perform = self.new_contents
 
278
        else:
 
279
            undo = self.new_contents
 
280
            perform = self.old_contents
 
281
        mode = None
 
282
        if undo is not None:
 
283
            try:
 
284
                mode = os.lstat(filename).st_mode
 
285
                if stat.S_ISLNK(mode):
 
286
                    mode = None
 
287
            except OSError, e:
 
288
                if e.errno != errno.ENOENT:
 
289
                    raise
 
290
                if conflict_handler.missing_for_rm(filename, undo) == "skip":
 
291
                    return
 
292
            undo(filename, conflict_handler, reverse=True)
 
293
        if perform is not None:
 
294
            perform(filename, conflict_handler, reverse=False)
 
295
            if mode is not None:
 
296
                os.chmod(filename, mode)
 
297
 
 
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)
 
326
 
 
327
 
 
328
class Diff3Merge(object):
 
329
    def __init__(self, file_id, base, other):
 
330
        self.file_id = file_id
 
331
        self.base = base
 
332
        self.other = other
 
333
 
 
334
    def __eq__(self, other):
 
335
        if not isinstance(other, Diff3Merge):
 
336
            return False
 
337
        return (self.base == other.base and 
 
338
                self.other == other.other and self.file_id == other.file_id)
 
339
 
 
340
    def __ne__(self, other):
 
341
        return not (self == other)
 
342
 
 
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:
 
348
            base = base_file
 
349
            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)
 
368
 
 
369
 
 
370
def CreateDir():
 
371
    """Convenience function to create a directory.
 
372
 
 
373
    :return: A ReplaceContents that will create a directory
 
374
    :rtype: `ReplaceContents`
 
375
    """
 
376
    return ReplaceContents(None, dir_create)
 
377
 
 
378
def DeleteDir():
 
379
    """Convenience function to delete a directory.
 
380
 
 
381
    :return: A ReplaceContents that will delete a directory
 
382
    :rtype: `ReplaceContents`
 
383
    """
 
384
    return ReplaceContents(dir_create, None)
 
385
 
 
386
def CreateFile(contents):
 
387
    """Convenience fucntion to create a file.
 
388
    
 
389
    :param contents: The contents of the file to create 
 
390
    :type contents: str
 
391
    :return: A ReplaceContents that will create a file 
 
392
    :rtype: `ReplaceContents`
 
393
    """
 
394
    return ReplaceContents(None, FileCreate(contents))
 
395
 
 
396
def DeleteFile(contents):
 
397
    """Convenience fucntion to delete a file.
 
398
    
 
399
    :param contents: The contents of the file to delete
 
400
    :type contents: str
 
401
    :return: A ReplaceContents that will delete a file 
 
402
    :rtype: `ReplaceContents`
 
403
    """
 
404
    return ReplaceContents(FileCreate(contents), None)
 
405
 
 
406
def ReplaceFileContents(old_contents, new_contents):
 
407
    """Convenience fucntion to replace the contents of a file.
 
408
    
 
409
    :param old_contents: The contents of the file to replace 
 
410
    :type old_contents: str
 
411
    :param new_contents: The contents to replace the file with
 
412
    :type new_contents: str
 
413
    :return: A ReplaceContents that will replace the contents of a file a file 
 
414
    :rtype: `ReplaceContents`
 
415
    """
 
416
    return ReplaceContents(FileCreate(old_contents), FileCreate(new_contents))
 
417
 
 
418
def CreateSymlink(target):
 
419
    """Convenience fucntion to create a symlink.
 
420
    
 
421
    :param target: The path the link should point to
 
422
    :type target: str
 
423
    :return: A ReplaceContents that will delete a file 
 
424
    :rtype: `ReplaceContents`
 
425
    """
 
426
    return ReplaceContents(None, SymlinkCreate(target))
 
427
 
 
428
def DeleteSymlink(target):
 
429
    """Convenience fucntion to delete a symlink.
 
430
    
 
431
    :param target: The path the link should point to
 
432
    :type target: str
 
433
    :return: A ReplaceContents that will delete a file 
 
434
    :rtype: `ReplaceContents`
 
435
    """
 
436
    return ReplaceContents(SymlinkCreate(target), None)
 
437
 
 
438
def ChangeTarget(old_target, new_target):
 
439
    """Convenience fucntion to change the target of a symlink.
 
440
    
 
441
    :param old_target: The current link target
 
442
    :type old_target: str
 
443
    :param new_target: The new link target to use
 
444
    :type new_target: str
 
445
    :return: A ReplaceContents that will delete a file 
 
446
    :rtype: `ReplaceContents`
 
447
    """
 
448
    return ReplaceContents(SymlinkCreate(old_target), SymlinkCreate(new_target))
 
449
 
 
450
 
 
451
class InvalidEntry(Exception):
 
452
    """Raise when a ChangesetEntry is invalid in some way"""
 
453
    def __init__(self, entry, problem):
 
454
        """Constructor.
 
455
 
 
456
        :param entry: The invalid ChangesetEntry
 
457
        :type entry: `ChangesetEntry`
 
458
        :param problem: The problem with the entry
 
459
        :type problem: str
 
460
        """
 
461
        msg = "Changeset entry for %s (%s) is invalid.\n%s" % (entry.id, 
 
462
                                                               entry.path, 
 
463
                                                               problem)
 
464
        Exception.__init__(self, msg)
 
465
        self.entry = entry
 
466
 
 
467
 
 
468
class SourceRootHasName(InvalidEntry):
 
469
    """This changeset entry has a name other than "", but its parent is !NULL"""
 
470
    def __init__(self, entry, name):
 
471
        """Constructor.
 
472
 
 
473
        :param entry: The invalid ChangesetEntry
 
474
        :type entry: `ChangesetEntry`
 
475
        :param name: The name of the entry
 
476
        :type name: str
 
477
        """
 
478
        msg = 'Child of !NULL is named "%s", not "./.".' % name
 
479
        InvalidEntry.__init__(self, entry, msg)
 
480
 
 
481
class NullIDAssigned(InvalidEntry):
 
482
    """The id !NULL was assigned to a real entry"""
 
483
    def __init__(self, entry):
 
484
        """Constructor.
 
485
 
 
486
        :param entry: The invalid ChangesetEntry
 
487
        :type entry: `ChangesetEntry`
 
488
        """
 
489
        msg = '"!NULL" id assigned to a file "%s".' % entry.path
 
490
        InvalidEntry.__init__(self, entry, msg)
 
491
 
 
492
class ParentIDIsSelf(InvalidEntry):
 
493
    """An entry is marked as its own parent"""
 
494
    def __init__(self, entry):
 
495
        """Constructor.
 
496
 
 
497
        :param entry: The invalid ChangesetEntry
 
498
        :type entry: `ChangesetEntry`
 
499
        """
 
500
        msg = 'file %s has "%s" id for both self id and parent id.' % \
 
501
            (entry.path, entry.id)
 
502
        InvalidEntry.__init__(self, entry, msg)
 
503
 
 
504
class ChangesetEntry(object):
 
505
    """An entry the changeset"""
 
506
    def __init__(self, id, parent, path):
 
507
        """Constructor. Sets parent and name assuming it was not
 
508
        renamed/created/deleted.
 
509
        :param id: The id associated with the entry
 
510
        :param parent: The id of the parent of this entry (or !NULL if no
 
511
        parent)
 
512
        :param path: The file path relative to the tree root of this entry
 
513
        """
 
514
        self.id = id
 
515
        self.path = path 
 
516
        self.new_path = path
 
517
        self.parent = parent
 
518
        self.new_parent = parent
 
519
        self.contents_change = None
 
520
        self.metadata_change = None
 
521
        if parent == NULL_ID and path !='./.':
 
522
            raise SourceRootHasName(self, path)
 
523
        if self.id == NULL_ID:
 
524
            raise NullIDAssigned(self)
 
525
        if self.id  == self.parent:
 
526
            raise ParentIDIsSelf(self)
 
527
 
 
528
    def __str__(self):
 
529
        return "ChangesetEntry(%s)" % self.id
 
530
 
 
531
    def __get_dir(self):
 
532
        if self.path is None:
 
533
            return None
 
534
        return os.path.dirname(self.path)
 
535
 
 
536
    def __set_dir(self, dir):
 
537
        self.path = os.path.join(dir, os.path.basename(self.path))
 
538
 
 
539
    dir = property(__get_dir, __set_dir)
 
540
    
 
541
    def __get_name(self):
 
542
        if self.path is None:
 
543
            return None
 
544
        return os.path.basename(self.path)
 
545
 
 
546
    def __set_name(self, name):
 
547
        self.path = os.path.join(os.path.dirname(self.path), name)
 
548
 
 
549
    name = property(__get_name, __set_name)
 
550
 
 
551
    def __get_new_dir(self):
 
552
        if self.new_path is None:
 
553
            return None
 
554
        return os.path.dirname(self.new_path)
 
555
 
 
556
    def __set_new_dir(self, dir):
 
557
        self.new_path = os.path.join(dir, os.path.basename(self.new_path))
 
558
 
 
559
    new_dir = property(__get_new_dir, __set_new_dir)
 
560
 
 
561
    def __get_new_name(self):
 
562
        if self.new_path is None:
 
563
            return None
 
564
        return os.path.basename(self.new_path)
 
565
 
 
566
    def __set_new_name(self, name):
 
567
        self.new_path = os.path.join(os.path.dirname(self.new_path), name)
 
568
 
 
569
    new_name = property(__get_new_name, __set_new_name)
 
570
 
 
571
    def needs_rename(self):
 
572
        """Determines whether the entry requires renaming.
 
573
 
 
574
        :rtype: bool
 
575
        """
 
576
 
 
577
        return (self.parent != self.new_parent or self.name != self.new_name)
 
578
 
 
579
    def is_deletion(self, reverse):
 
580
        """Return true if applying the entry would delete a file/directory.
 
581
 
 
582
        :param reverse: if true, the changeset is being applied in reverse
 
583
        :rtype: bool
 
584
        """
 
585
        return ((self.new_parent is None and not reverse) or 
 
586
                (self.parent is None and reverse))
 
587
 
 
588
    def is_creation(self, reverse):
 
589
        """Return true if applying the entry would create a file/directory.
 
590
 
 
591
        :param reverse: if true, the changeset is being applied in reverse
 
592
        :rtype: bool
 
593
        """
 
594
        return ((self.parent is None and not reverse) or 
 
595
                (self.new_parent is None and reverse))
 
596
 
 
597
    def is_creation_or_deletion(self):
 
598
        """Return true if applying the entry would create or delete a 
 
599
        file/directory.
 
600
 
 
601
        :rtype: bool
 
602
        """
 
603
        return self.parent is None or self.new_parent is None
 
604
 
 
605
    def get_cset_path(self, mod=False):
 
606
        """Determine the path of the entry according to the changeset.
 
607
 
 
608
        :param changeset: The changeset to derive the path from
 
609
        :type changeset: `Changeset`
 
610
        :param mod: If true, generate the MOD path.  Otherwise, generate the \
 
611
        ORIG path.
 
612
        :return: the path of the entry, or None if it did not exist in the \
 
613
        requested tree.
 
614
        :rtype: str or NoneType
 
615
        """
 
616
        if mod:
 
617
            if self.new_parent == NULL_ID:
 
618
                return "./."
 
619
            elif self.new_parent is None:
 
620
                return None
 
621
            return self.new_path
 
622
        else:
 
623
            if self.parent == NULL_ID:
 
624
                return "./."
 
625
            elif self.parent is None:
 
626
                return None
 
627
            return self.path
 
628
 
 
629
    def summarize_name(self, reverse=False):
 
630
        """Produce a one-line summary of the filename.  Indicates renames as
 
631
        old => new, indicates creation as None => new, indicates deletion as
 
632
        old => None.
 
633
 
 
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
        :rtype: str
 
639
        """
 
640
        orig_path = self.get_cset_path(False)
 
641
        mod_path = self.get_cset_path(True)
 
642
        if orig_path is not None:
 
643
            orig_path = orig_path[2:]
 
644
        if mod_path is not None:
 
645
            mod_path = mod_path[2:]
 
646
        if orig_path == mod_path:
 
647
            return orig_path
 
648
        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):
 
656
        """Determine the full pathname to rename to
 
657
 
 
658
        :param id_map: The map of ids to filenames for the tree
 
659
        :type id_map: Dictionary
 
660
        :param changeset: The changeset to get data from
 
661
        :type changeset: `Changeset`
 
662
        :param reverse: If true, we're applying the changeset in reverse
 
663
        :type reverse: bool
 
664
        :rtype: str
 
665
        """
 
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
 
679
 
 
680
        if to_name is None:
 
681
            return None
 
682
 
 
683
        if parent == NULL_ID or parent is None:
 
684
            if to_name != '.':
 
685
                raise SourceRootHasName(self, to_name)
 
686
            else:
 
687
                return '.'
 
688
        if from_dir == to_dir:
 
689
            dir = os.path.dirname(id_map[self.id])
 
690
        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)
 
694
        if from_name == to_name:
 
695
            name = os.path.basename(id_map[self.id])
 
696
        else:
 
697
            name = to_name
 
698
            assert(from_name is None or from_name == os.path.basename(id_map[self.id]))
 
699
        return os.path.join(dir, name)
 
700
 
 
701
    def is_boring(self):
 
702
        """Determines whether the entry does nothing
 
703
        
 
704
        :return: True if the entry does no renames or content changes
 
705
        :rtype: bool
 
706
        """
 
707
        if self.contents_change is not None:
 
708
            return False
 
709
        elif self.metadata_change is not None:
 
710
            return False
 
711
        elif self.parent != self.new_parent:
 
712
            return False
 
713
        elif self.name != self.new_name:
 
714
            return False
 
715
        else:
 
716
            return True
 
717
 
 
718
    def apply(self, filename, conflict_handler, reverse=False):
 
719
        """Applies the file content and/or metadata changes.
 
720
 
 
721
        :param filename: the filename of the entry
 
722
        :type filename: str
 
723
        :param reverse: If true, apply the changes in reverse
 
724
        :type reverse: bool
 
725
        """
 
726
        if self.is_deletion(reverse) and self.metadata_change is not None:
 
727
            self.metadata_change.apply(filename, conflict_handler, reverse)
 
728
        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)
 
732
 
 
733
class IDPresent(Exception):
 
734
    def __init__(self, id):
 
735
        msg = "Cannot add entry because that id has already been used:\n%s" %\
 
736
            id
 
737
        Exception.__init__(self, msg)
 
738
        self.id = id
 
739
 
 
740
class Changeset(object):
 
741
    """A set of changes to apply"""
 
742
    def __init__(self):
 
743
        self.entries = {}
 
744
 
 
745
    def add_entry(self, entry):
 
746
        """Add an entry to the list of entries"""
 
747
        if self.entries.has_key(entry.id):
 
748
            raise IDPresent(entry.id)
 
749
        self.entries[entry.id] = entry
 
750
 
 
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
 
 
767
def get_rename_entries(changeset, inventory, reverse):
 
768
    """Return a list of entries that will be renamed.  Entries are sorted from
 
769
    longest to shortest source path and from shortest to longest target path.
 
770
 
 
771
    :param changeset: The changeset to look in
 
772
    :type changeset: `Changeset`
 
773
    :param inventory: The source of current tree paths for the given ids
 
774
    :type inventory: Dictionary
 
775
    :param reverse: If true, the changeset is being applied in reverse
 
776
    :type reverse: bool
 
777
    :return: source entries and target entries as a tuple
 
778
    :rtype: (List, List)
 
779
    """
 
780
    source_entries = [x for x in changeset.entries.itervalues() 
 
781
                      if x.needs_rename()]
 
782
    # these are done from longest path to shortest, to avoid deleting a
 
783
    # parent before its children are deleted/renamed 
 
784
    def longest_to_shortest(entry):
 
785
        path = inventory.get(entry.id)
 
786
        if path is None:
 
787
            return 0
 
788
        else:
 
789
            return len(path)
 
790
    my_sort(source_entries, longest_to_shortest, reverse=True)
 
791
 
 
792
    target_entries = source_entries[:]
 
793
    # These are done from shortest to longest path, to avoid creating a
 
794
    # child before its parent has been created/renamed
 
795
    def shortest_to_longest(entry):
 
796
        path = entry.get_new_path(inventory, changeset, reverse)
 
797
        if path is None:
 
798
            return 0
 
799
        else:
 
800
            return len(path)
 
801
    my_sort(target_entries, shortest_to_longest)
 
802
    return (source_entries, target_entries)
 
803
 
 
804
def rename_to_temp_delete(source_entries, inventory, dir, temp_dir, 
 
805
                          conflict_handler, reverse):
 
806
    """Delete and rename entries as appropriate.  Entries are renamed to temp
 
807
    names.  A map of id -> temp name (or None, for deletions) is returned.
 
808
 
 
809
    :param source_entries: The entries to rename and delete
 
810
    :type source_entries: List of `ChangesetEntry`
 
811
    :param inventory: The map of id -> filename in the current tree
 
812
    :type inventory: Dictionary
 
813
    :param dir: The directory to apply changes to
 
814
    :type dir: str
 
815
    :param reverse: Apply changes in reverse
 
816
    :type reverse: bool
 
817
    :return: a mapping of id to temporary name
 
818
    :rtype: Dictionary
 
819
    """
 
820
    temp_name = {}
 
821
    for i in range(len(source_entries)):
 
822
        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)
 
826
            temp_name[entry.id] = None
 
827
 
 
828
        else:
 
829
            to_name = os.path.join(temp_dir, str(i))
 
830
            src_path = inventory.get(entry.id)
 
831
            if src_path is not None:
 
832
                src_path = os.path.join(dir, src_path)
 
833
                try:
 
834
                    os.rename(src_path, to_name)
 
835
                    temp_name[entry.id] = to_name
 
836
                except OSError, e:
 
837
                    if e.errno != errno.ENOENT:
 
838
                        raise
 
839
                    if conflict_handler.missing_for_rename(src_path) == "skip":
 
840
                        continue
 
841
 
 
842
    return temp_name
 
843
 
 
844
 
 
845
def rename_to_new_create(changed_inventory, target_entries, inventory, 
 
846
                         changeset, dir, conflict_handler, reverse):
 
847
    """Rename entries with temp names to their final names, create new files.
 
848
 
 
849
    :param changed_inventory: A mapping of id to temporary name
 
850
    :type changed_inventory: Dictionary
 
851
    :param target_entries: The entries to apply changes to
 
852
    :type target_entries: List of `ChangesetEntry`
 
853
    :param changeset: The changeset to apply
 
854
    :type changeset: `Changeset`
 
855
    :param dir: The directory to apply changes to
 
856
    :type dir: str
 
857
    :param reverse: If true, apply changes in reverse
 
858
    :type reverse: bool
 
859
    """
 
860
    for entry in target_entries:
 
861
        new_tree_path = entry.get_new_path(inventory, changeset, reverse)
 
862
        if new_tree_path is None:
 
863
            continue
 
864
        new_path = os.path.join(dir, new_tree_path)
 
865
        old_path = changed_inventory.get(entry.id)
 
866
        if os.path.exists(new_path):
 
867
            if conflict_handler.target_exists(entry, new_path, old_path) == \
 
868
                "skip":
 
869
                continue
 
870
        if entry.is_creation(reverse):
 
871
            entry.apply(new_path, conflict_handler, reverse)
 
872
            changed_inventory[entry.id] = new_tree_path
 
873
        else:
 
874
            if old_path is None:
 
875
                continue
 
876
            try:
 
877
                os.rename(old_path, new_path)
 
878
                changed_inventory[entry.id] = new_tree_path
 
879
            except OSError, e:
 
880
                raise Exception ("%s is missing" % new_path)
 
881
 
 
882
class TargetExists(Exception):
 
883
    def __init__(self, entry, target):
 
884
        msg = "The path %s already exists" % target
 
885
        Exception.__init__(self, msg)
 
886
        self.entry = entry
 
887
        self.target = target
 
888
 
 
889
class RenameConflict(Exception):
 
890
    def __init__(self, id, this_name, base_name, other_name):
 
891
        msg = """Trees all have different names for a file
 
892
 this: %s
 
893
 base: %s
 
894
other: %s
 
895
   id: %s""" % (this_name, base_name, other_name, id)
 
896
        Exception.__init__(self, msg)
 
897
        self.this_name = this_name
 
898
        self.base_name = base_name
 
899
        self_other_name = other_name
 
900
 
 
901
class MoveConflict(Exception):
 
902
    def __init__(self, id, this_parent, base_parent, other_parent):
 
903
        msg = """The file is in different directories in every tree
 
904
 this: %s
 
905
 base: %s
 
906
other: %s
 
907
   id: %s""" % (this_parent, base_parent, other_parent, id)
 
908
        Exception.__init__(self, msg)
 
909
        self.this_parent = this_parent
 
910
        self.base_parent = base_parent
 
911
        self_other_parent = other_parent
 
912
 
 
913
class MergeConflict(Exception):
 
914
    def __init__(self, this_path):
 
915
        Exception.__init__(self, "Conflict applying changes to %s" % this_path)
 
916
        self.this_path = this_path
 
917
 
 
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
 
 
933
class WrongOldContents(Exception):
 
934
    def __init__(self, filename):
 
935
        msg = "Contents mismatch deleting %s" % filename
 
936
        self.filename = filename
 
937
        Exception.__init__(self, msg)
 
938
 
 
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)
 
943
        self.filename = filename
 
944
        Exception.__init__(self, msg)
 
945
 
 
946
class RemoveContentsConflict(Exception):
 
947
    def __init__(self, filename):
 
948
        msg = "Conflict deleting %s, which has different contents in BASE"\
 
949
            " and THIS" % filename
 
950
        self.filename = filename
 
951
        Exception.__init__(self, msg)
 
952
 
 
953
class DeletingNonEmptyDirectory(Exception):
 
954
    def __init__(self, filename):
 
955
        msg = "Trying to remove dir %s while it still had files" % filename
 
956
        self.filename = filename
 
957
        Exception.__init__(self, msg)
 
958
 
 
959
 
 
960
class PatchTargetMissing(Exception):
 
961
    def __init__(self, filename):
 
962
        msg = "Attempt to patch %s, which does not exist" % filename
 
963
        Exception.__init__(self, msg)
 
964
        self.filename = filename
 
965
 
 
966
class MissingPermsFile(Exception):
 
967
    def __init__(self, filename):
 
968
        msg = "Attempt to change permissions on  %s, which does not exist" %\
 
969
            filename
 
970
        Exception.__init__(self, msg)
 
971
        self.filename = filename
 
972
 
 
973
class MissingForRm(Exception):
 
974
    def __init__(self, filename):
 
975
        msg = "Attempt to remove missing path %s" % filename
 
976
        Exception.__init__(self, msg)
 
977
        self.filename = filename
 
978
 
 
979
 
 
980
class MissingForRename(Exception):
 
981
    def __init__(self, filename):
 
982
        msg = "Attempt to move missing path %s" % (filename)
 
983
        Exception.__init__(self, msg)
 
984
        self.filename = filename
 
985
 
 
986
class NewContentsConflict(Exception):
 
987
    def __init__(self, filename):
 
988
        msg = "Conflicting contents for new file %s" % (filename)
 
989
        Exception.__init__(self, msg)
 
990
 
 
991
 
 
992
class MissingForMerge(Exception):
 
993
    def __init__(self, filename):
 
994
        msg = "The file %s was modified, but does not exist in this tree"\
 
995
            % (filename)
 
996
        Exception.__init__(self, msg)
 
997
 
 
998
 
 
999
class ExceptionConflictHandler(object):
 
1000
    """Default handler for merge exceptions.
 
1001
 
 
1002
    This throws an error on any kind of conflict.  Conflict handlers can
 
1003
    descend from this class if they have a better way to handle some or
 
1004
    all types of conflict.
 
1005
    """
 
1006
    def __init__(self, dir):
 
1007
        self.dir = dir
 
1008
    
 
1009
    def missing_parent(self, pathname):
 
1010
        parent = os.path.dirname(pathname)
 
1011
        raise Exception("Parent directory missing for %s" % pathname)
 
1012
 
 
1013
    def dir_exists(self, pathname):
 
1014
        raise Exception("Directory already exists for %s" % pathname)
 
1015
 
 
1016
    def failed_hunks(self, pathname):
 
1017
        raise Exception("Failed to apply some hunks for %s" % pathname)
 
1018
 
 
1019
    def target_exists(self, entry, target, old_path):
 
1020
        raise TargetExists(entry, target)
 
1021
 
 
1022
    def rename_conflict(self, id, this_name, base_name, other_name):
 
1023
        raise RenameConflict(id, this_name, base_name, other_name)
 
1024
 
 
1025
    def move_conflict(self, id, this_dir, base_dir, other_dir):
 
1026
        raise MoveConflict(id, this_dir, base_dir, other_dir)
 
1027
 
 
1028
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
 
1029
        os.unlink(new_file)
 
1030
        raise MergeConflict(this_path)
 
1031
 
 
1032
    def permission_conflict(self, this_path, base_path, other_path):
 
1033
        raise MergePermissionConflict(this_path, base_path, other_path)
 
1034
 
 
1035
    def wrong_old_contents(self, filename, expected_contents):
 
1036
        raise WrongOldContents(filename)
 
1037
 
 
1038
    def rem_contents_conflict(self, filename, this_contents, base_contents):
 
1039
        raise RemoveContentsConflict(filename)
 
1040
 
 
1041
    def wrong_old_perms(self, filename, old_perms, new_perms):
 
1042
        raise WrongOldPermissions(filename, old_perms, new_perms)
 
1043
 
 
1044
    def rmdir_non_empty(self, filename):
 
1045
        raise DeletingNonEmptyDirectory(filename)
 
1046
 
 
1047
    def link_name_exists(self, filename):
 
1048
        raise TargetExists(filename)
 
1049
 
 
1050
    def patch_target_missing(self, filename, contents):
 
1051
        raise PatchTargetMissing(filename)
 
1052
 
 
1053
    def missing_for_chmod(self, filename):
 
1054
        raise MissingPermsFile(filename)
 
1055
 
 
1056
    def missing_for_rm(self, filename, change):
 
1057
        raise MissingForRm(filename)
 
1058
 
 
1059
    def missing_for_rename(self, filename):
 
1060
        raise MissingForRename(filename)
 
1061
 
 
1062
    def missing_for_merge(self, file_id, other_path):
 
1063
        raise MissingForMerge(other_path)
 
1064
 
 
1065
    def new_contents_conflict(self, filename, other_contents):
 
1066
        raise NewContentsConflict(filename)
 
1067
 
 
1068
    def finalize(self):
 
1069
        pass
 
1070
 
 
1071
def apply_changeset(changeset, inventory, dir, conflict_handler=None, 
 
1072
                    reverse=False):
 
1073
    """Apply a changeset to a directory.
 
1074
 
 
1075
    :param changeset: The changes to perform
 
1076
    :type changeset: `Changeset`
 
1077
    :param inventory: The mapping of id to filename for the directory
 
1078
    :type inventory: Dictionary
 
1079
    :param dir: The path of the directory to apply the changes to
 
1080
    :type dir: str
 
1081
    :param reverse: If true, apply the changes in reverse
 
1082
    :type reverse: bool
 
1083
    :return: The mapping of the changed entries
 
1084
    :rtype: Dictionary
 
1085
    """
 
1086
    if conflict_handler is None:
 
1087
        conflict_handler = ExceptionConflictHandler(dir)
 
1088
    temp_dir = os.path.join(dir, "bzr-tree-change")
 
1089
    try:
 
1090
        os.mkdir(temp_dir)
 
1091
    except OSError, e:
 
1092
        if e.errno == errno.EEXIST:
 
1093
            try:
 
1094
                os.rmdir(temp_dir)
 
1095
            except OSError, e:
 
1096
                if e.errno == errno.ENOTEMPTY:
 
1097
                    raise OldFailedTreeOp()
 
1098
            os.mkdir(temp_dir)
 
1099
        else:
 
1100
            raise
 
1101
    
 
1102
    #apply changes that don't affect filenames
 
1103
    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)
 
1107
 
 
1108
    # Apply renames in stages, to minimize conflicts:
 
1109
    # Only files whose name or parent change are interesting, because their
 
1110
    # target name may exist in the source tree.  If a directory's name changes,
 
1111
    # that doesn't make its children interesting.
 
1112
    (source_entries, target_entries) = get_rename_entries(changeset, inventory,
 
1113
                                                          reverse)
 
1114
 
 
1115
    changed_inventory = rename_to_temp_delete(source_entries, inventory, dir,
 
1116
                                              temp_dir, conflict_handler,
 
1117
                                              reverse)
 
1118
 
 
1119
    rename_to_new_create(changed_inventory, target_entries, inventory,
 
1120
                         changeset, dir, conflict_handler, reverse)
 
1121
    os.rmdir(temp_dir)
 
1122
    return changed_inventory
 
1123
 
 
1124
 
 
1125
def apply_changeset_tree(cset, tree, reverse=False):
 
1126
    r_inventory = {}
 
1127
    for entry in tree.source_inventory().itervalues():
 
1128
        inventory[entry.id] = entry.path
 
1129
    new_inventory = apply_changeset(cset, r_inventory, tree.root,
 
1130
                                    reverse=reverse)
 
1131
    new_entries, remove_entries = \
 
1132
        get_inventory_change(inventory, new_inventory, cset, reverse)
 
1133
    tree.update_source_inventory(new_entries, remove_entries)
 
1134
 
 
1135
 
 
1136
def get_inventory_change(inventory, new_inventory, cset, reverse=False):
 
1137
    new_entries = {}
 
1138
    remove_entries = []
 
1139
    for entry in cset.entries.itervalues():
 
1140
        if entry.needs_rename():
 
1141
            new_path = entry.get_new_path(inventory, cset)
 
1142
            if new_path is None:
 
1143
                remove_entries.append(entry.id)
 
1144
            else:
 
1145
                new_entries[new_path] = entry.id
 
1146
    return new_entries, remove_entries
 
1147
 
 
1148
 
 
1149
def print_changeset(cset):
 
1150
    """Print all non-boring changeset entries
 
1151
    
 
1152
    :param cset: The changeset to print
 
1153
    :type cset: `Changeset`
 
1154
    """
 
1155
    for entry in cset.entries.itervalues():
 
1156
        if entry.is_boring():
 
1157
            continue
 
1158
        print entry.id
 
1159
        print entry.summarize_name(cset)
 
1160
 
 
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
 
1286
        Exception.__init__(self, msg)
 
1287
        self.full_path = full_path
 
1288
        self.stat_result = stat_result
 
1289
 
 
1290
def generate_changeset(tree_a, tree_b, interesting_ids=None):
 
1291
    return ChangesetGenerator(tree_a, tree_b, interesting_ids)()
 
1292
 
 
1293
class ChangesetGenerator(object):
 
1294
    def __init__(self, tree_a, tree_b, interesting_ids=None):
 
1295
        object.__init__(self)
 
1296
        self.tree_a = tree_a
 
1297
        self.tree_b = tree_b
 
1298
        self._interesting_ids = interesting_ids
 
1299
 
 
1300
    def iter_both_tree_ids(self):
 
1301
        for file_id in self.tree_a:
 
1302
            yield file_id
 
1303
        for file_id in self.tree_b:
 
1304
            if file_id not in self.tree_a:
 
1305
                yield file_id
 
1306
 
 
1307
    def __call__(self):
 
1308
        cset = Changeset()
 
1309
        for file_id in self.iter_both_tree_ids():
 
1310
            cs_entry = self.make_entry(file_id)
 
1311
            if cs_entry is not None and not cs_entry.is_boring():
 
1312
                cset.add_entry(cs_entry)
 
1313
 
 
1314
        for entry in list(cset.entries.itervalues()):
 
1315
            if entry.parent != entry.new_parent:
 
1316
                if not cset.entries.has_key(entry.parent) and\
 
1317
                    entry.parent != NULL_ID and entry.parent is not None:
 
1318
                    parent_entry = self.make_boring_entry(entry.parent)
 
1319
                    cset.add_entry(parent_entry)
 
1320
                if not cset.entries.has_key(entry.new_parent) and\
 
1321
                    entry.new_parent != NULL_ID and \
 
1322
                    entry.new_parent is not None:
 
1323
                    parent_entry = self.make_boring_entry(entry.new_parent)
 
1324
                    cset.add_entry(parent_entry)
 
1325
        return cset
 
1326
 
 
1327
    def iter_inventory(self, tree):
 
1328
        for file_id in tree:
 
1329
            yield self.get_entry(file_id, tree)
 
1330
 
 
1331
    def get_entry(self, file_id, tree):
 
1332
        if not tree.has_or_had_id(file_id):
 
1333
            return None
 
1334
        return tree.tree.inventory[file_id]
 
1335
 
 
1336
    def get_entry_parent(self, entry):
 
1337
        if entry is None:
 
1338
            return None
 
1339
        return entry.parent_id
 
1340
 
 
1341
    def get_path(self, file_id, tree):
 
1342
        if not tree.has_or_had_id(file_id):
 
1343
            return None
 
1344
        path = tree.id2path(file_id)
 
1345
        if path == '':
 
1346
            return './.'
 
1347
        else:
 
1348
            return path
 
1349
 
 
1350
    def make_basic_entry(self, file_id, only_interesting):
 
1351
        entry_a = self.get_entry(file_id, self.tree_a)
 
1352
        entry_b = self.get_entry(file_id, self.tree_b)
 
1353
        if only_interesting and not self.is_interesting(entry_a, entry_b):
 
1354
            return None
 
1355
        parent = self.get_entry_parent(entry_a)
 
1356
        path = self.get_path(file_id, self.tree_a)
 
1357
        cs_entry = ChangesetEntry(file_id, parent, path)
 
1358
        new_parent = self.get_entry_parent(entry_b)
 
1359
 
 
1360
        new_path = self.get_path(file_id, self.tree_b)
 
1361
 
 
1362
        cs_entry.new_path = new_path
 
1363
        cs_entry.new_parent = new_parent
 
1364
        return cs_entry
 
1365
 
 
1366
    def is_interesting(self, entry_a, entry_b):
 
1367
        if self._interesting_ids is None:
 
1368
            return True
 
1369
        if entry_a is not None:
 
1370
            file_id = entry_a.file_id
 
1371
        elif entry_b is not None:
 
1372
            file_id = entry_b.file_id
 
1373
        else:
 
1374
            return False
 
1375
        return file_id in self._interesting_ids
 
1376
 
 
1377
    def make_boring_entry(self, id):
 
1378
        cs_entry = self.make_basic_entry(id, only_interesting=False)
 
1379
        if cs_entry.is_creation_or_deletion():
 
1380
            return self.make_entry(id, only_interesting=False)
 
1381
        else:
 
1382
            return cs_entry
 
1383
        
 
1384
 
 
1385
    def make_entry(self, id, only_interesting=True):
 
1386
        cs_entry = self.make_basic_entry(id, only_interesting)
 
1387
 
 
1388
        if cs_entry is None:
 
1389
            return None
 
1390
        if id in self.tree_a and id in self.tree_b:
 
1391
            a_sha1 = self.tree_a.get_file_sha1(id)
 
1392
            b_sha1 = self.tree_b.get_file_sha1(id)
 
1393
            if None not in (a_sha1, b_sha1) and a_sha1 == b_sha1:
 
1394
                return cs_entry
 
1395
 
 
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)
 
1406
        return cs_entry
 
1407
 
 
1408
    def make_mode_change(self, stat_a, stat_b):
 
1409
        mode_a = None
 
1410
        if stat_a is not None and not stat.S_ISLNK(stat_a.st_mode):
 
1411
            mode_a = stat_a.st_mode & 0777
 
1412
        mode_b = None
 
1413
        if stat_b is not None and not stat.S_ISLNK(stat_b.st_mode):
 
1414
            mode_b = stat_b.st_mode & 0777
 
1415
        if mode_a == mode_b:
 
1416
            return None
 
1417
        return ChangeUnixPermissions(mode_a, mode_b)
 
1418
 
 
1419
    def make_contents_change(self, full_path_a, stat_a, full_path_b, stat_b):
 
1420
        if stat_a is None and stat_b is None:
 
1421
            return None
 
1422
        if None not in (stat_a, stat_b) and stat.S_ISDIR(stat_a.st_mode) and\
 
1423
            stat.S_ISDIR(stat_b.st_mode):
 
1424
            return None
 
1425
        if None not in (stat_a, stat_b) and stat.S_ISREG(stat_a.st_mode) and\
 
1426
            stat.S_ISREG(stat_b.st_mode):
 
1427
            if stat_a.st_ino == stat_b.st_ino and \
 
1428
                stat_a.st_dev == stat_b.st_dev:
 
1429
                return None
 
1430
 
 
1431
        a_contents = self.get_contents(stat_a, full_path_a)
 
1432
        b_contents = self.get_contents(stat_b, full_path_b)
 
1433
        if a_contents == b_contents:
 
1434
            return None
 
1435
        return ReplaceContents(a_contents, b_contents)
 
1436
 
 
1437
    def get_contents(self, stat_result, full_path):
 
1438
        if stat_result is None:
 
1439
            return None
 
1440
        elif stat.S_ISREG(stat_result.st_mode):
 
1441
            return FileCreate(file(full_path, "rb").read())
 
1442
        elif stat.S_ISDIR(stat_result.st_mode):
 
1443
            return dir_create
 
1444
        elif stat.S_ISLNK(stat_result.st_mode):
 
1445
            return SymlinkCreate(os.readlink(full_path))
 
1446
        else:
 
1447
            raise UnsupportedFiletype(full_path, stat_result)
 
1448
 
 
1449
    def lstat(self, full_path):
 
1450
        stat_result = None
 
1451
        if full_path is not None:
 
1452
            try:
 
1453
                stat_result = os.lstat(full_path)
 
1454
            except OSError, e:
 
1455
                if e.errno != errno.ENOENT:
 
1456
                    raise
 
1457
        return stat_result
 
1458
 
 
1459
 
 
1460
def full_path(entry, tree):
 
1461
    return os.path.join(tree.root, entry.path)
 
1462
 
 
1463
def new_delete_entry(entry, tree, inventory, delete):
 
1464
    if entry.path == "":
 
1465
        parent = NULL_ID
 
1466
    else:
 
1467
        parent = inventory[dirname(entry.path)].id
 
1468
    cs_entry = ChangesetEntry(parent, entry.path)
 
1469
    if delete:
 
1470
        cs_entry.new_path = None
 
1471
        cs_entry.new_parent = None
 
1472
    else:
 
1473
        cs_entry.path = None
 
1474
        cs_entry.parent = None
 
1475
    full_path = full_path(entry, tree)
 
1476
    status = os.lstat(full_path)
 
1477
    if stat.S_ISDIR(file_stat.st_mode):
 
1478
        action = dir_create
 
1479
    
 
1480
 
 
1481
 
 
1482
        
 
1483
# XXX: Can't we unify this with the regular inventory object
 
1484
class Inventory(object):
 
1485
    def __init__(self, inventory):
 
1486
        self.inventory = inventory
 
1487
        self.rinventory = None
 
1488
 
 
1489
    def get_rinventory(self):
 
1490
        if self.rinventory is None:
 
1491
            self.rinventory  = invert_dict(self.inventory)
 
1492
        return self.rinventory
 
1493
 
 
1494
    def get_path(self, id):
 
1495
        return self.inventory.get(id)
 
1496
 
 
1497
    def get_name(self, id):
 
1498
        path = self.get_path(id)
 
1499
        if path is None:
 
1500
            return None
 
1501
        else:
 
1502
            return os.path.basename(path)
 
1503
 
 
1504
    def get_dir(self, id):
 
1505
        path = self.get_path(id)
 
1506
        if path == "":
 
1507
            return None
 
1508
        if path is None:
 
1509
            return None
 
1510
        return os.path.dirname(path)
 
1511
 
 
1512
    def get_parent(self, id):
 
1513
        if self.get_path(id) is None:
 
1514
            return None
 
1515
        directory = self.get_dir(id)
 
1516
        if directory == '.':
 
1517
            directory = './.'
 
1518
        if directory is None:
 
1519
            return NULL_ID
 
1520
        return self.get_rinventory().get(directory)