~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/changeset.py

  • Committer: Martin Pool
  • Date: 2005-07-16 00:07:40 UTC
  • mfrom: (909.1.5)
  • Revision ID: mbp@sourcefrog.net-20050716000740-f2dcb8894a23fd2d
- merge aaron's bugfix branch
  up to abentley@panoramicfeedback.com-20050715134354-78f2bca607acb415

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