~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/_changeset.py

  • Committer: Erik Bågfors
  • Date: 2006-02-03 19:50:59 UTC
  • mto: (1185.50.77 bzr-jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: erik@bagfors.nu-20060203195059-1cf8ff5aa68de0ea
Support for plugins to register log formatters and set default formatter
Also, change one command line option for "log"

Show diffs side-by-side

added added

removed removed

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