~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

[merge] update from bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2007 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 by Aaron Bentley
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
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
19
19
# TODO: 'bzr resolve' should accept a directory name and work from that 
20
20
# point down
21
21
 
 
22
# TODO: bzr revert should resolve; even when reverting the whole tree
 
23
# or particular directories
 
24
 
22
25
import os
23
 
 
24
 
from bzrlib.lazy_import import lazy_import
25
 
lazy_import(globals(), """
26
26
import errno
27
27
 
28
 
from bzrlib import (
29
 
    builtins,
30
 
    commands,
31
 
    errors,
32
 
    osutils,
33
 
    rio,
34
 
    trace,
35
 
    )
36
 
""")
37
 
from bzrlib.option import Option
38
 
 
39
 
 
40
 
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
41
 
 
42
 
 
43
 
class cmd_conflicts(commands.Command):
 
28
import bzrlib.status
 
29
from bzrlib.branch import Branch
 
30
from bzrlib.errors import BzrCommandError, NotConflicted
 
31
from bzrlib.commands import register_command
 
32
from bzrlib.workingtree import CONFLICT_SUFFIXES
 
33
from bzrlib.osutils import rename
 
34
 
 
35
class cmd_conflicts(bzrlib.commands.Command):
44
36
    """List files with conflicts.
45
 
 
46
 
    Merge will do its best to combine the changes in two branches, but there
47
 
    are some kinds of problems only a human can fix.  When it encounters those,
48
 
    it will mark a conflict.  A conflict means that you need to fix something,
49
 
    before you should commit.
50
 
 
51
 
    Conflicts normally are listed as short, human-readable messages.  If --text
52
 
    is supplied, the pathnames of files with text conflicts are listed,
53
 
    instead.  (This is useful for editing all files with text conflicts.)
54
 
 
55
 
    Use bzr resolve when you have fixed a problem.
56
 
 
57
 
    See also bzr resolve.
 
37
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
 
38
    files.)
58
39
    """
59
 
    takes_options = [
60
 
            Option('text',
61
 
                   help='List paths of files with text conflicts.'),
62
 
        ]
63
 
 
64
 
    def run(self, text=False):
65
 
        from bzrlib.workingtree import WorkingTree
66
 
        wt = WorkingTree.open_containing(u'.')[0]
67
 
        for conflict in wt.conflicts():
68
 
            if text:
69
 
                if conflict.typestring != 'text conflict':
70
 
                    continue
71
 
                self.outf.write(conflict.path + '\n')
72
 
            else:
73
 
                self.outf.write(str(conflict) + '\n')
74
 
 
75
 
 
76
 
class cmd_resolve(commands.Command):
 
40
    def run(self):
 
41
        for path in Branch.open_containing(u'.')[0].working_tree().iter_conflicts():
 
42
            print path
 
43
 
 
44
class cmd_resolve(bzrlib.commands.Command):
77
45
    """Mark a conflict as resolved.
78
 
 
79
 
    Merge will do its best to combine the changes in two branches, but there
80
 
    are some kinds of problems only a human can fix.  When it encounters those,
81
 
    it will mark a conflict.  A conflict means that you need to fix something,
82
 
    before you should commit.
83
 
 
84
 
    Once you have fixed a problem, use "bzr resolve" to automatically mark
85
 
    text conflicts as fixed, resolve FILE to mark a specific conflict as
86
 
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
87
 
 
88
 
    See also bzr conflicts.
89
46
    """
90
47
    aliases = ['resolved']
91
48
    takes_args = ['file*']
92
 
    takes_options = [
93
 
            Option('all', help='Resolve all conflicts in this tree.'),
94
 
            ]
 
49
    takes_options = ['all']
95
50
    def run(self, file_list=None, all=False):
96
 
        from bzrlib.workingtree import WorkingTree
97
 
        if all:
98
 
            if file_list:
99
 
                raise errors.BzrCommandError("If --all is specified,"
100
 
                                             " no FILE may be provided")
101
 
            tree = WorkingTree.open_containing('.')[0]
102
 
            resolve(tree)
 
51
        if file_list is None:
 
52
            if not all:
 
53
                raise BzrCommandError(
 
54
                    "command 'resolve' needs one or more FILE, or --all")
 
55
            tree = Branch.open_containing(u'.')[0].working_tree()
 
56
            file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
103
57
        else:
104
 
            tree, file_list = builtins.tree_files(file_list)
105
 
            if file_list is None:
106
 
                un_resolved, resolved = tree.auto_resolve()
107
 
                if len(un_resolved) > 0:
108
 
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
109
 
                    trace.note('Remaining conflicts:')
110
 
                    for conflict in un_resolved:
111
 
                        trace.note(conflict)
112
 
                    return 1
 
58
            if all:
 
59
                raise BzrCommandError(
 
60
                    "If --all is specified, no FILE may be provided")
 
61
        for filename in file_list:
 
62
            failures = 0
 
63
            for suffix in CONFLICT_SUFFIXES:
 
64
                try:
 
65
                    os.unlink(filename+suffix)
 
66
                except OSError, e:
 
67
                    if e.errno != errno.ENOENT:
 
68
                        raise
 
69
                    else:
 
70
                        failures += 1
 
71
            if failures == len(CONFLICT_SUFFIXES):
 
72
                if not os.path.exists(filename):
 
73
                    print "%s does not exist" % filename
113
74
                else:
114
 
                    trace.note('All conflicts resolved.')
115
 
                    return 0
116
 
            else:
117
 
                resolve(tree, file_list)
118
 
 
119
 
 
120
 
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
121
 
    """Resolve some or all of the conflicts in a working tree.
122
 
 
123
 
    :param paths: If None, resolve all conflicts.  Otherwise, select only
124
 
        specified conflicts.
125
 
    :param recursive: If True, then elements of paths which are directories
126
 
        have all their children resolved, etc.  When invoked as part of
127
 
        recursive commands like revert, this should be True.  For commands
128
 
        or applications wishing finer-grained control, like the resolve
129
 
        command, this should be False.
130
 
    :ignore_misses: If False, warnings will be printed if the supplied paths
131
 
        do not have conflicts.
132
 
    """
133
 
    tree.lock_tree_write()
134
 
    try:
135
 
        tree_conflicts = tree.conflicts()
136
 
        if paths is None:
137
 
            new_conflicts = ConflictList()
138
 
            selected_conflicts = tree_conflicts
139
 
        else:
140
 
            new_conflicts, selected_conflicts = \
141
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses,
142
 
                    recursive)
143
 
        try:
144
 
            tree.set_conflicts(new_conflicts)
145
 
        except errors.UnsupportedOperation:
146
 
            pass
147
 
        selected_conflicts.remove_files(tree)
148
 
    finally:
149
 
        tree.unlock()
150
 
 
 
75
                    print "%s is not conflicted" % filename
151
76
 
152
77
def restore(filename):
153
78
    """\
156
81
    """
157
82
    conflicted = False
158
83
    try:
159
 
        osutils.rename(filename + ".THIS", filename)
 
84
        rename(filename + ".THIS", filename)
160
85
        conflicted = True
161
86
    except OSError, e:
162
87
        if e.errno != errno.ENOENT:
174
99
        if e.errno != errno.ENOENT:
175
100
            raise
176
101
    if not conflicted:
177
 
        raise errors.NotConflicted(filename)
178
 
 
179
 
 
180
 
class ConflictList(object):
181
 
    """List of conflicts.
182
 
 
183
 
    Typically obtained from WorkingTree.conflicts()
184
 
 
185
 
    Can be instantiated from stanzas or from Conflict subclasses.
186
 
    """
187
 
 
188
 
    def __init__(self, conflicts=None):
189
 
        object.__init__(self)
190
 
        if conflicts is None:
191
 
            self.__list = []
192
 
        else:
193
 
            self.__list = conflicts
194
 
 
195
 
    def is_empty(self):
196
 
        return len(self.__list) == 0
197
 
 
198
 
    def __len__(self):
199
 
        return len(self.__list)
200
 
 
201
 
    def __iter__(self):
202
 
        return iter(self.__list)
203
 
 
204
 
    def __getitem__(self, key):
205
 
        return self.__list[key]
206
 
 
207
 
    def append(self, conflict):
208
 
        return self.__list.append(conflict)
209
 
 
210
 
    def __eq__(self, other_list):
211
 
        return list(self) == list(other_list)
212
 
 
213
 
    def __ne__(self, other_list):
214
 
        return not (self == other_list)
215
 
 
216
 
    def __repr__(self):
217
 
        return "ConflictList(%r)" % self.__list
218
 
 
219
 
    @staticmethod
220
 
    def from_stanzas(stanzas):
221
 
        """Produce a new ConflictList from an iterable of stanzas"""
222
 
        conflicts = ConflictList()
223
 
        for stanza in stanzas:
224
 
            conflicts.append(Conflict.factory(**stanza.as_dict()))
225
 
        return conflicts
226
 
 
227
 
    def to_stanzas(self):
228
 
        """Generator of stanzas"""
229
 
        for conflict in self:
230
 
            yield conflict.as_stanza()
231
 
            
232
 
    def to_strings(self):
233
 
        """Generate strings for the provided conflicts"""
234
 
        for conflict in self:
235
 
            yield str(conflict)
236
 
 
237
 
    def remove_files(self, tree):
238
 
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
239
 
        for conflict in self:
240
 
            if not conflict.has_files:
241
 
                continue
242
 
            for suffix in CONFLICT_SUFFIXES:
243
 
                try:
244
 
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
245
 
                except OSError, e:
246
 
                    if e.errno != errno.ENOENT:
247
 
                        raise
248
 
 
249
 
    def select_conflicts(self, tree, paths, ignore_misses=False,
250
 
                         recurse=False):
251
 
        """Select the conflicts associated with paths in a tree.
252
 
        
253
 
        File-ids are also used for this.
254
 
        :return: a pair of ConflictLists: (not_selected, selected)
255
 
        """
256
 
        path_set = set(paths)
257
 
        ids = {}
258
 
        selected_paths = set()
259
 
        new_conflicts = ConflictList()
260
 
        selected_conflicts = ConflictList()
261
 
        for path in paths:
262
 
            file_id = tree.path2id(path)
263
 
            if file_id is not None:
264
 
                ids[file_id] = path
265
 
 
266
 
        for conflict in self:
267
 
            selected = False
268
 
            for key in ('path', 'conflict_path'):
269
 
                cpath = getattr(conflict, key, None)
270
 
                if cpath is None:
271
 
                    continue
272
 
                if cpath in path_set:
273
 
                    selected = True
274
 
                    selected_paths.add(cpath)
275
 
                if recurse:
276
 
                    if osutils.is_inside_any(path_set, cpath):
277
 
                        selected = True
278
 
                        selected_paths.add(cpath)
279
 
 
280
 
            for key in ('file_id', 'conflict_file_id'):
281
 
                cfile_id = getattr(conflict, key, None)
282
 
                if cfile_id is None:
283
 
                    continue
284
 
                try:
285
 
                    cpath = ids[cfile_id]
286
 
                except KeyError:
287
 
                    continue
288
 
                selected = True
289
 
                selected_paths.add(cpath)
290
 
            if selected:
291
 
                selected_conflicts.append(conflict)
292
 
            else:
293
 
                new_conflicts.append(conflict)
294
 
        if ignore_misses is not True:
295
 
            for path in [p for p in paths if p not in selected_paths]:
296
 
                if not os.path.exists(tree.abspath(path)):
297
 
                    print "%s does not exist" % path
298
 
                else:
299
 
                    print "%s is not conflicted" % path
300
 
        return new_conflicts, selected_conflicts
301
 
 
302
 
 
303
 
class Conflict(object):
304
 
    """Base class for all types of conflict"""
305
 
 
306
 
    has_files = False
307
 
 
308
 
    def __init__(self, path, file_id=None):
309
 
        self.path = path
310
 
        # warn turned off, because the factory blindly transfers the Stanza
311
 
        # values to __init__ and Stanza is purely a Unicode api.
312
 
        self.file_id = osutils.safe_file_id(file_id, warn=False)
313
 
 
314
 
    def as_stanza(self):
315
 
        s = rio.Stanza(type=self.typestring, path=self.path)
316
 
        if self.file_id is not None:
317
 
            # Stanza requires Unicode apis
318
 
            s.add('file_id', self.file_id.decode('utf8'))
319
 
        return s
320
 
 
321
 
    def _cmp_list(self):
322
 
        return [type(self), self.path, self.file_id]
323
 
 
324
 
    def __cmp__(self, other):
325
 
        if getattr(other, "_cmp_list", None) is None:
326
 
            return -1
327
 
        return cmp(self._cmp_list(), other._cmp_list())
328
 
 
329
 
    def __hash__(self):
330
 
        return hash((type(self), self.path, self.file_id))
331
 
 
332
 
    def __eq__(self, other):
333
 
        return self.__cmp__(other) == 0
334
 
 
335
 
    def __ne__(self, other):
336
 
        return not self.__eq__(other)
337
 
 
338
 
    def __str__(self):
339
 
        return self.format % self.__dict__
340
 
 
341
 
    def __repr__(self):
342
 
        rdict = dict(self.__dict__)
343
 
        rdict['class'] = self.__class__.__name__
344
 
        return self.rformat % rdict
345
 
 
346
 
    @staticmethod
347
 
    def factory(type, **kwargs):
348
 
        global ctype
349
 
        return ctype[type](**kwargs)
350
 
 
351
 
    @staticmethod
352
 
    def sort_key(conflict):
353
 
        if conflict.path is not None:
354
 
            return conflict.path, conflict.typestring
355
 
        elif getattr(conflict, "conflict_path", None) is not None:
356
 
            return conflict.conflict_path, conflict.typestring
357
 
        else:
358
 
            return None, conflict.typestring
359
 
 
360
 
 
361
 
class PathConflict(Conflict):
362
 
    """A conflict was encountered merging file paths"""
363
 
 
364
 
    typestring = 'path conflict'
365
 
 
366
 
    format = 'Path conflict: %(path)s / %(conflict_path)s'
367
 
 
368
 
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
369
 
    def __init__(self, path, conflict_path=None, file_id=None):
370
 
        Conflict.__init__(self, path, file_id)
371
 
        self.conflict_path = conflict_path
372
 
 
373
 
    def as_stanza(self):
374
 
        s = Conflict.as_stanza(self)
375
 
        if self.conflict_path is not None:
376
 
            s.add('conflict_path', self.conflict_path)
377
 
        return s
378
 
 
379
 
 
380
 
class ContentsConflict(PathConflict):
381
 
    """The files are of different types, or not present"""
382
 
 
383
 
    has_files = True
384
 
 
385
 
    typestring = 'contents conflict'
386
 
 
387
 
    format = 'Contents conflict in %(path)s'
388
 
 
389
 
 
390
 
class TextConflict(PathConflict):
391
 
    """The merge algorithm could not resolve all differences encountered."""
392
 
 
393
 
    has_files = True
394
 
 
395
 
    typestring = 'text conflict'
396
 
 
397
 
    format = 'Text conflict in %(path)s'
398
 
 
399
 
 
400
 
class HandledConflict(Conflict):
401
 
    """A path problem that has been provisionally resolved.
402
 
    This is intended to be a base class.
403
 
    """
404
 
 
405
 
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
406
 
    
407
 
    def __init__(self, action, path, file_id=None):
408
 
        Conflict.__init__(self, path, file_id)
409
 
        self.action = action
410
 
 
411
 
    def _cmp_list(self):
412
 
        return Conflict._cmp_list(self) + [self.action]
413
 
 
414
 
    def as_stanza(self):
415
 
        s = Conflict.as_stanza(self)
416
 
        s.add('action', self.action)
417
 
        return s
418
 
 
419
 
 
420
 
class HandledPathConflict(HandledConflict):
421
 
    """A provisionally-resolved path problem involving two paths.
422
 
    This is intended to be a base class.
423
 
    """
424
 
 
425
 
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
426
 
        " %(file_id)r, %(conflict_file_id)r)"
427
 
 
428
 
    def __init__(self, action, path, conflict_path, file_id=None,
429
 
                 conflict_file_id=None):
430
 
        HandledConflict.__init__(self, action, path, file_id)
431
 
        self.conflict_path = conflict_path 
432
 
        # warn turned off, because the factory blindly transfers the Stanza
433
 
        # values to __init__.
434
 
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
435
 
                                                     warn=False)
436
 
        
437
 
    def _cmp_list(self):
438
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
439
 
                                                  self.conflict_file_id]
440
 
 
441
 
    def as_stanza(self):
442
 
        s = HandledConflict.as_stanza(self)
443
 
        s.add('conflict_path', self.conflict_path)
444
 
        if self.conflict_file_id is not None:
445
 
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
446
 
            
447
 
        return s
448
 
 
449
 
 
450
 
class DuplicateID(HandledPathConflict):
451
 
    """Two files want the same file_id."""
452
 
 
453
 
    typestring = 'duplicate id'
454
 
 
455
 
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
456
 
 
457
 
 
458
 
class DuplicateEntry(HandledPathConflict):
459
 
    """Two directory entries want to have the same name."""
460
 
 
461
 
    typestring = 'duplicate'
462
 
 
463
 
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
464
 
 
465
 
 
466
 
class ParentLoop(HandledPathConflict):
467
 
    """An attempt to create an infinitely-looping directory structure.
468
 
    This is rare, but can be produced like so:
469
 
 
470
 
    tree A:
471
 
      mv foo/bar
472
 
    tree B:
473
 
      mv bar/foo
474
 
    merge A and B
475
 
    """
476
 
 
477
 
    typestring = 'parent loop'
478
 
 
479
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
480
 
 
481
 
 
482
 
class UnversionedParent(HandledConflict):
483
 
    """An attempt to version an file whose parent directory is not versioned.
484
 
    Typically, the result of a merge where one tree unversioned the directory
485
 
    and the other added a versioned file to it.
486
 
    """
487
 
 
488
 
    typestring = 'unversioned parent'
489
 
 
490
 
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
491
 
             ' children.  %(action)s.'
492
 
 
493
 
 
494
 
class MissingParent(HandledConflict):
495
 
    """An attempt to add files to a directory that is not present.
496
 
    Typically, the result of a merge where THIS deleted the directory and
497
 
    the OTHER added a file to it.
498
 
    See also: DeletingParent (same situation, reversed THIS and OTHER)
499
 
    """
500
 
 
501
 
    typestring = 'missing parent'
502
 
 
503
 
    format = 'Conflict adding files to %(path)s.  %(action)s.'
504
 
 
505
 
 
506
 
class DeletingParent(HandledConflict):
507
 
    """An attempt to add files to a directory that is not present.
508
 
    Typically, the result of a merge where one OTHER deleted the directory and
509
 
    the THIS added a file to it.
510
 
    """
511
 
 
512
 
    typestring = 'deleting parent'
513
 
 
514
 
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
515
 
             "%(action)s."
516
 
 
517
 
 
518
 
class NonDirectoryParent(HandledConflict):
519
 
    """An attempt to add files to a directory that is not a director or
520
 
    an attempt to change the kind of a directory with files.
521
 
    """
522
 
 
523
 
    typestring = 'non-directory parent'
524
 
 
525
 
    format = "Conflict: %(path)s is not a directory, but has files in it."\
526
 
             "  %(action)s."
527
 
 
528
 
ctype = {}
529
 
 
530
 
 
531
 
def register_types(*conflict_types):
532
 
    """Register a Conflict subclass for serialization purposes"""
533
 
    global ctype
534
 
    for conflict_type in conflict_types:
535
 
        ctype[conflict_type.typestring] = conflict_type
536
 
 
537
 
 
538
 
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
539
 
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
540
 
               DeletingParent, NonDirectoryParent)
 
102
        raise NotConflicted(filename)