1
# Copyright (C) 2005 Aaron Bentley, Canonical Ltd
 
 
3
# This program is free software; you can redistribute it and/or modify
 
 
4
# it under the terms of the GNU General Public License as published by
 
 
5
# the Free Software Foundation; either version 2 of the License, or
 
 
6
# (at your option) any later version.
 
 
8
# This program is distributed in the hope that it will be useful,
 
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
11
# GNU General Public License for more details.
 
 
13
# You should have received a copy of the GNU General Public License
 
 
14
# along with this program; if not, write to the Free Software
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
17
# TODO: Move this into builtins
 
 
19
# TODO: 'bzr resolve' should accept a directory name and work from that 
 
 
24
from bzrlib.lazy_import import lazy_import
 
 
25
lazy_import(globals(), """
 
 
37
from bzrlib.option import Option
 
 
40
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
 
43
class cmd_conflicts(commands.Command):
 
 
44
    """List files with conflicts.
 
 
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.
 
 
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.)
 
 
55
    Use bzr resolve when you have fixed a problem.
 
 
61
                   help='List paths of files with text conflicts.'),
 
 
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():
 
 
69
                if conflict.typestring != 'text conflict':
 
 
71
                self.outf.write(conflict.path + '\n')
 
 
73
                self.outf.write(str(conflict) + '\n')
 
 
76
class cmd_resolve(commands.Command):
 
 
77
    """Mark a conflict as resolved.
 
 
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.
 
 
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.
 
 
88
    See also bzr conflicts.
 
 
90
    aliases = ['resolved']
 
 
91
    takes_args = ['file*']
 
 
93
            Option('all', help='Resolve all conflicts in this tree.'),
 
 
95
    def run(self, file_list=None, all=False):
 
 
96
        from bzrlib.workingtree import WorkingTree
 
 
99
                raise errors.BzrCommandError("If --all is specified,"
 
 
100
                                             " no FILE may be provided")
 
 
101
            tree = WorkingTree.open_containing('.')[0]
 
 
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:
 
 
114
                    trace.note('All conflicts resolved.')
 
 
117
                resolve(tree, file_list)
 
 
120
def resolve(tree, paths=None, ignore_misses=False):
 
 
121
    tree.lock_tree_write()
 
 
123
        tree_conflicts = tree.conflicts()
 
 
125
            new_conflicts = ConflictList()
 
 
126
            selected_conflicts = tree_conflicts
 
 
128
            new_conflicts, selected_conflicts = \
 
 
129
                tree_conflicts.select_conflicts(tree, paths, ignore_misses)
 
 
131
            tree.set_conflicts(new_conflicts)
 
 
132
        except errors.UnsupportedOperation:
 
 
134
        selected_conflicts.remove_files(tree)
 
 
139
def restore(filename):
 
 
141
    Restore a conflicted file to the state it was in before merging.
 
 
142
    Only text restoration supported at present.
 
 
146
        osutils.rename(filename + ".THIS", filename)
 
 
149
        if e.errno != errno.ENOENT:
 
 
152
        os.unlink(filename + ".BASE")
 
 
155
        if e.errno != errno.ENOENT:
 
 
158
        os.unlink(filename + ".OTHER")
 
 
161
        if e.errno != errno.ENOENT:
 
 
164
        raise errors.NotConflicted(filename)
 
 
167
class ConflictList(object):
 
 
168
    """List of conflicts.
 
 
170
    Typically obtained from WorkingTree.conflicts()
 
 
172
    Can be instantiated from stanzas or from Conflict subclasses.
 
 
175
    def __init__(self, conflicts=None):
 
 
176
        object.__init__(self)
 
 
177
        if conflicts is None:
 
 
180
            self.__list = conflicts
 
 
183
        return len(self.__list) == 0
 
 
186
        return len(self.__list)
 
 
189
        return iter(self.__list)
 
 
191
    def __getitem__(self, key):
 
 
192
        return self.__list[key]
 
 
194
    def append(self, conflict):
 
 
195
        return self.__list.append(conflict)
 
 
197
    def __eq__(self, other_list):
 
 
198
        return list(self) == list(other_list)
 
 
200
    def __ne__(self, other_list):
 
 
201
        return not (self == other_list)
 
 
204
        return "ConflictList(%r)" % self.__list
 
 
207
    def from_stanzas(stanzas):
 
 
208
        """Produce a new ConflictList from an iterable of stanzas"""
 
 
209
        conflicts = ConflictList()
 
 
210
        for stanza in stanzas:
 
 
211
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
 
214
    def to_stanzas(self):
 
 
215
        """Generator of stanzas"""
 
 
216
        for conflict in self:
 
 
217
            yield conflict.as_stanza()
 
 
219
    def to_strings(self):
 
 
220
        """Generate strings for the provided conflicts"""
 
 
221
        for conflict in self:
 
 
224
    def remove_files(self, tree):
 
 
225
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
 
 
226
        for conflict in self:
 
 
227
            if not conflict.has_files:
 
 
229
            for suffix in CONFLICT_SUFFIXES:
 
 
231
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
 
 
233
                    if e.errno != errno.ENOENT:
 
 
236
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
 
238
        """Select the conflicts associated with paths in a tree.
 
 
240
        File-ids are also used for this.
 
 
241
        :return: a pair of ConflictLists: (not_selected, selected)
 
 
243
        path_set = set(paths)
 
 
245
        selected_paths = set()
 
 
246
        new_conflicts = ConflictList()
 
 
247
        selected_conflicts = ConflictList()
 
 
249
            file_id = tree.path2id(path)
 
 
250
            if file_id is not None:
 
 
253
        for conflict in self:
 
 
255
            for key in ('path', 'conflict_path'):
 
 
256
                cpath = getattr(conflict, key, None)
 
 
259
                if cpath in path_set:
 
 
261
                    selected_paths.add(cpath)
 
 
263
                    if osutils.is_inside_any(path_set, cpath):
 
 
265
                        selected_paths.add(cpath)
 
 
267
            for key in ('file_id', 'conflict_file_id'):
 
 
268
                cfile_id = getattr(conflict, key, None)
 
 
272
                    cpath = ids[cfile_id]
 
 
276
                selected_paths.add(cpath)
 
 
278
                selected_conflicts.append(conflict)
 
 
280
                new_conflicts.append(conflict)
 
 
281
        if ignore_misses is not True:
 
 
282
            for path in [p for p in paths if p not in selected_paths]:
 
 
283
                if not os.path.exists(tree.abspath(path)):
 
 
284
                    print "%s does not exist" % path
 
 
286
                    print "%s is not conflicted" % path
 
 
287
        return new_conflicts, selected_conflicts
 
 
290
class Conflict(object):
 
 
291
    """Base class for all types of conflict"""
 
 
295
    def __init__(self, path, file_id=None):
 
 
297
        # warn turned off, because the factory blindly transfers the Stanza
 
 
298
        # values to __init__ and Stanza is purely a Unicode api.
 
 
299
        self.file_id = osutils.safe_file_id(file_id, warn=False)
 
 
302
        s = rio.Stanza(type=self.typestring, path=self.path)
 
 
303
        if self.file_id is not None:
 
 
304
            # Stanza requires Unicode apis
 
 
305
            s.add('file_id', self.file_id.decode('utf8'))
 
 
309
        return [type(self), self.path, self.file_id]
 
 
311
    def __cmp__(self, other):
 
 
312
        if getattr(other, "_cmp_list", None) is None:
 
 
314
        return cmp(self._cmp_list(), other._cmp_list())
 
 
317
        return hash((type(self), self.path, self.file_id))
 
 
319
    def __eq__(self, other):
 
 
320
        return self.__cmp__(other) == 0
 
 
322
    def __ne__(self, other):
 
 
323
        return not self.__eq__(other)
 
 
326
        return self.format % self.__dict__
 
 
329
        rdict = dict(self.__dict__)
 
 
330
        rdict['class'] = self.__class__.__name__
 
 
331
        return self.rformat % rdict
 
 
334
    def factory(type, **kwargs):
 
 
336
        return ctype[type](**kwargs)
 
 
339
    def sort_key(conflict):
 
 
340
        if conflict.path is not None:
 
 
341
            return conflict.path, conflict.typestring
 
 
342
        elif getattr(conflict, "conflict_path", None) is not None:
 
 
343
            return conflict.conflict_path, conflict.typestring
 
 
345
            return None, conflict.typestring
 
 
348
class PathConflict(Conflict):
 
 
349
    """A conflict was encountered merging file paths"""
 
 
351
    typestring = 'path conflict'
 
 
353
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
 
355
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
 
356
    def __init__(self, path, conflict_path=None, file_id=None):
 
 
357
        Conflict.__init__(self, path, file_id)
 
 
358
        self.conflict_path = conflict_path
 
 
361
        s = Conflict.as_stanza(self)
 
 
362
        if self.conflict_path is not None:
 
 
363
            s.add('conflict_path', self.conflict_path)
 
 
367
class ContentsConflict(PathConflict):
 
 
368
    """The files are of different types, or not present"""
 
 
372
    typestring = 'contents conflict'
 
 
374
    format = 'Contents conflict in %(path)s'
 
 
377
class TextConflict(PathConflict):
 
 
378
    """The merge algorithm could not resolve all differences encountered."""
 
 
382
    typestring = 'text conflict'
 
 
384
    format = 'Text conflict in %(path)s'
 
 
387
class HandledConflict(Conflict):
 
 
388
    """A path problem that has been provisionally resolved.
 
 
389
    This is intended to be a base class.
 
 
392
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
 
394
    def __init__(self, action, path, file_id=None):
 
 
395
        Conflict.__init__(self, path, file_id)
 
 
399
        return Conflict._cmp_list(self) + [self.action]
 
 
402
        s = Conflict.as_stanza(self)
 
 
403
        s.add('action', self.action)
 
 
407
class HandledPathConflict(HandledConflict):
 
 
408
    """A provisionally-resolved path problem involving two paths.
 
 
409
    This is intended to be a base class.
 
 
412
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
 
413
        " %(file_id)r, %(conflict_file_id)r)"
 
 
415
    def __init__(self, action, path, conflict_path, file_id=None,
 
 
416
                 conflict_file_id=None):
 
 
417
        HandledConflict.__init__(self, action, path, file_id)
 
 
418
        self.conflict_path = conflict_path 
 
 
419
        # warn turned off, because the factory blindly transfers the Stanza
 
 
420
        # values to __init__.
 
 
421
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
 
 
425
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
 
426
                                                  self.conflict_file_id]
 
 
429
        s = HandledConflict.as_stanza(self)
 
 
430
        s.add('conflict_path', self.conflict_path)
 
 
431
        if self.conflict_file_id is not None:
 
 
432
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
 
437
class DuplicateID(HandledPathConflict):
 
 
438
    """Two files want the same file_id."""
 
 
440
    typestring = 'duplicate id'
 
 
442
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
 
445
class DuplicateEntry(HandledPathConflict):
 
 
446
    """Two directory entries want to have the same name."""
 
 
448
    typestring = 'duplicate'
 
 
450
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
 
453
class ParentLoop(HandledPathConflict):
 
 
454
    """An attempt to create an infinitely-looping directory structure.
 
 
455
    This is rare, but can be produced like so:
 
 
464
    typestring = 'parent loop'
 
 
466
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
 
469
class UnversionedParent(HandledConflict):
 
 
470
    """An attempt to version an file whose parent directory is not versioned.
 
 
471
    Typically, the result of a merge where one tree unversioned the directory
 
 
472
    and the other added a versioned file to it.
 
 
475
    typestring = 'unversioned parent'
 
 
477
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
 
478
             ' children.  %(action)s.'
 
 
481
class MissingParent(HandledConflict):
 
 
482
    """An attempt to add files to a directory that is not present.
 
 
483
    Typically, the result of a merge where THIS deleted the directory and
 
 
484
    the OTHER added a file to it.
 
 
485
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
 
488
    typestring = 'missing parent'
 
 
490
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
 
493
class DeletingParent(HandledConflict):
 
 
494
    """An attempt to add files to a directory that is not present.
 
 
495
    Typically, the result of a merge where one OTHER deleted the directory and
 
 
496
    the THIS added a file to it.
 
 
499
    typestring = 'deleting parent'
 
 
501
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
 
508
def register_types(*conflict_types):
 
 
509
    """Register a Conflict subclass for serialization purposes"""
 
 
511
    for conflict_type in conflict_types:
 
 
512
        ctype[conflict_type.typestring] = conflict_type
 
 
515
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
 
516
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,