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.
 
 
59
    takes_options = [Option('text', help='list text conflicts by pathname')]
 
 
61
    def run(self, text=False):
 
 
62
        from bzrlib.workingtree import WorkingTree
 
 
63
        wt = WorkingTree.open_containing(u'.')[0]
 
 
64
        for conflict in wt.conflicts():
 
 
66
                if conflict.typestring != 'text conflict':
 
 
68
                self.outf.write(conflict.path + '\n')
 
 
70
                self.outf.write(str(conflict) + '\n')
 
 
73
class cmd_resolve(commands.Command):
 
 
74
    """Mark a conflict as resolved.
 
 
76
    Merge will do its best to combine the changes in two branches, but there
 
 
77
    are some kinds of problems only a human can fix.  When it encounters those,
 
 
78
    it will mark a conflict.  A conflict means that you need to fix something,
 
 
79
    before you should commit.
 
 
81
    Once you have fixed a problem, use "bzr resolve" to automatically mark
 
 
82
    text conflicts as fixed, resolve FILE to mark a specific conflict as
 
 
83
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
 
 
85
    See also bzr conflicts.
 
 
87
    aliases = ['resolved']
 
 
88
    takes_args = ['file*']
 
 
89
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
 
 
90
    def run(self, file_list=None, all=False):
 
 
91
        from bzrlib.workingtree import WorkingTree
 
 
94
                raise errors.BzrCommandError("If --all is specified,"
 
 
95
                                             " no FILE may be provided")
 
 
96
            tree = WorkingTree.open_containing('.')[0]
 
 
99
            tree, file_list = builtins.tree_files(file_list)
 
 
100
            if file_list is None:
 
 
101
                un_resolved, resolved = tree.auto_resolve()
 
 
102
                if len(un_resolved) > 0:
 
 
103
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
 
 
104
                    trace.note('Remaining conflicts:')
 
 
105
                    for conflict in un_resolved:
 
 
109
                    trace.note('All conflicts resolved.')
 
 
112
                resolve(tree, file_list)
 
 
115
def resolve(tree, paths=None, ignore_misses=False):
 
 
116
    tree.lock_tree_write()
 
 
118
        tree_conflicts = tree.conflicts()
 
 
120
            new_conflicts = ConflictList()
 
 
121
            selected_conflicts = tree_conflicts
 
 
123
            new_conflicts, selected_conflicts = \
 
 
124
                tree_conflicts.select_conflicts(tree, paths, ignore_misses)
 
 
126
            tree.set_conflicts(new_conflicts)
 
 
127
        except errors.UnsupportedOperation:
 
 
129
        selected_conflicts.remove_files(tree)
 
 
134
def restore(filename):
 
 
136
    Restore a conflicted file to the state it was in before merging.
 
 
137
    Only text restoration supported at present.
 
 
141
        osutils.rename(filename + ".THIS", filename)
 
 
144
        if e.errno != errno.ENOENT:
 
 
147
        os.unlink(filename + ".BASE")
 
 
150
        if e.errno != errno.ENOENT:
 
 
153
        os.unlink(filename + ".OTHER")
 
 
156
        if e.errno != errno.ENOENT:
 
 
159
        raise errors.NotConflicted(filename)
 
 
162
class ConflictList(object):
 
 
163
    """List of conflicts.
 
 
165
    Typically obtained from WorkingTree.conflicts()
 
 
167
    Can be instantiated from stanzas or from Conflict subclasses.
 
 
170
    def __init__(self, conflicts=None):
 
 
171
        object.__init__(self)
 
 
172
        if conflicts is None:
 
 
175
            self.__list = conflicts
 
 
178
        return len(self.__list) == 0
 
 
181
        return len(self.__list)
 
 
184
        return iter(self.__list)
 
 
186
    def __getitem__(self, key):
 
 
187
        return self.__list[key]
 
 
189
    def append(self, conflict):
 
 
190
        return self.__list.append(conflict)
 
 
192
    def __eq__(self, other_list):
 
 
193
        return list(self) == list(other_list)
 
 
195
    def __ne__(self, other_list):
 
 
196
        return not (self == other_list)
 
 
199
        return "ConflictList(%r)" % self.__list
 
 
202
    def from_stanzas(stanzas):
 
 
203
        """Produce a new ConflictList from an iterable of stanzas"""
 
 
204
        conflicts = ConflictList()
 
 
205
        for stanza in stanzas:
 
 
206
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
 
209
    def to_stanzas(self):
 
 
210
        """Generator of stanzas"""
 
 
211
        for conflict in self:
 
 
212
            yield conflict.as_stanza()
 
 
214
    def to_strings(self):
 
 
215
        """Generate strings for the provided conflicts"""
 
 
216
        for conflict in self:
 
 
219
    def remove_files(self, tree):
 
 
220
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
 
 
221
        for conflict in self:
 
 
222
            if not conflict.has_files:
 
 
224
            for suffix in CONFLICT_SUFFIXES:
 
 
226
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
 
 
228
                    if e.errno != errno.ENOENT:
 
 
231
    def select_conflicts(self, tree, paths, ignore_misses=False):
 
 
232
        """Select the conflicts associated with paths in a tree.
 
 
234
        File-ids are also used for this.
 
 
235
        :return: a pair of ConflictLists: (not_selected, selected)
 
 
237
        path_set = set(paths)
 
 
239
        selected_paths = set()
 
 
240
        new_conflicts = ConflictList()
 
 
241
        selected_conflicts = ConflictList()
 
 
243
            file_id = tree.path2id(path)
 
 
244
            if file_id is not None:
 
 
247
        for conflict in self:
 
 
249
            for key in ('path', 'conflict_path'):
 
 
250
                cpath = getattr(conflict, key, None)
 
 
253
                if cpath in path_set:
 
 
255
                    selected_paths.add(cpath)
 
 
256
            for key in ('file_id', 'conflict_file_id'):
 
 
257
                cfile_id = getattr(conflict, key, None)
 
 
261
                    cpath = ids[cfile_id]
 
 
265
                selected_paths.add(cpath)
 
 
267
                selected_conflicts.append(conflict)
 
 
269
                new_conflicts.append(conflict)
 
 
270
        if ignore_misses is not True:
 
 
271
            for path in [p for p in paths if p not in selected_paths]:
 
 
272
                if not os.path.exists(tree.abspath(path)):
 
 
273
                    print "%s does not exist" % path
 
 
275
                    print "%s is not conflicted" % path
 
 
276
        return new_conflicts, selected_conflicts
 
 
279
class Conflict(object):
 
 
280
    """Base class for all types of conflict"""
 
 
284
    def __init__(self, path, file_id=None):
 
 
286
        # warn turned off, because the factory blindly transfers the Stanza
 
 
287
        # values to __init__ and Stanza is purely a Unicode api.
 
 
288
        self.file_id = osutils.safe_file_id(file_id, warn=False)
 
 
291
        s = rio.Stanza(type=self.typestring, path=self.path)
 
 
292
        if self.file_id is not None:
 
 
293
            # Stanza requires Unicode apis
 
 
294
            s.add('file_id', self.file_id.decode('utf8'))
 
 
298
        return [type(self), self.path, self.file_id]
 
 
300
    def __cmp__(self, other):
 
 
301
        if getattr(other, "_cmp_list", None) is None:
 
 
303
        return cmp(self._cmp_list(), other._cmp_list())
 
 
306
        return hash((type(self), self.path, self.file_id))
 
 
308
    def __eq__(self, other):
 
 
309
        return self.__cmp__(other) == 0
 
 
311
    def __ne__(self, other):
 
 
312
        return not self.__eq__(other)
 
 
315
        return self.format % self.__dict__
 
 
318
        rdict = dict(self.__dict__)
 
 
319
        rdict['class'] = self.__class__.__name__
 
 
320
        return self.rformat % rdict
 
 
323
    def factory(type, **kwargs):
 
 
325
        return ctype[type](**kwargs)
 
 
328
    def sort_key(conflict):
 
 
329
        if conflict.path is not None:
 
 
330
            return conflict.path, conflict.typestring
 
 
331
        elif getattr(conflict, "conflict_path", None) is not None:
 
 
332
            return conflict.conflict_path, conflict.typestring
 
 
334
            return None, conflict.typestring
 
 
337
class PathConflict(Conflict):
 
 
338
    """A conflict was encountered merging file paths"""
 
 
340
    typestring = 'path conflict'
 
 
342
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
 
344
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
 
345
    def __init__(self, path, conflict_path=None, file_id=None):
 
 
346
        Conflict.__init__(self, path, file_id)
 
 
347
        self.conflict_path = conflict_path
 
 
350
        s = Conflict.as_stanza(self)
 
 
351
        if self.conflict_path is not None:
 
 
352
            s.add('conflict_path', self.conflict_path)
 
 
356
class ContentsConflict(PathConflict):
 
 
357
    """The files are of different types, or not present"""
 
 
361
    typestring = 'contents conflict'
 
 
363
    format = 'Contents conflict in %(path)s'
 
 
366
class TextConflict(PathConflict):
 
 
367
    """The merge algorithm could not resolve all differences encountered."""
 
 
371
    typestring = 'text conflict'
 
 
373
    format = 'Text conflict in %(path)s'
 
 
376
class HandledConflict(Conflict):
 
 
377
    """A path problem that has been provisionally resolved.
 
 
378
    This is intended to be a base class.
 
 
381
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
 
383
    def __init__(self, action, path, file_id=None):
 
 
384
        Conflict.__init__(self, path, file_id)
 
 
388
        return Conflict._cmp_list(self) + [self.action]
 
 
391
        s = Conflict.as_stanza(self)
 
 
392
        s.add('action', self.action)
 
 
396
class HandledPathConflict(HandledConflict):
 
 
397
    """A provisionally-resolved path problem involving two paths.
 
 
398
    This is intended to be a base class.
 
 
401
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
 
402
        " %(file_id)r, %(conflict_file_id)r)"
 
 
404
    def __init__(self, action, path, conflict_path, file_id=None,
 
 
405
                 conflict_file_id=None):
 
 
406
        HandledConflict.__init__(self, action, path, file_id)
 
 
407
        self.conflict_path = conflict_path 
 
 
408
        # warn turned off, because the factory blindly transfers the Stanza
 
 
409
        # values to __init__.
 
 
410
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
 
 
414
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
 
415
                                                  self.conflict_file_id]
 
 
418
        s = HandledConflict.as_stanza(self)
 
 
419
        s.add('conflict_path', self.conflict_path)
 
 
420
        if self.conflict_file_id is not None:
 
 
421
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
 
426
class DuplicateID(HandledPathConflict):
 
 
427
    """Two files want the same file_id."""
 
 
429
    typestring = 'duplicate id'
 
 
431
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
 
434
class DuplicateEntry(HandledPathConflict):
 
 
435
    """Two directory entries want to have the same name."""
 
 
437
    typestring = 'duplicate'
 
 
439
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
 
442
class ParentLoop(HandledPathConflict):
 
 
443
    """An attempt to create an infinitely-looping directory structure.
 
 
444
    This is rare, but can be produced like so:
 
 
453
    typestring = 'parent loop'
 
 
455
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
 
458
class UnversionedParent(HandledConflict):
 
 
459
    """An attempt to version an file whose parent directory is not versioned.
 
 
460
    Typically, the result of a merge where one tree unversioned the directory
 
 
461
    and the other added a versioned file to it.
 
 
464
    typestring = 'unversioned parent'
 
 
466
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
 
467
             ' children.  %(action)s.'
 
 
470
class MissingParent(HandledConflict):
 
 
471
    """An attempt to add files to a directory that is not present.
 
 
472
    Typically, the result of a merge where THIS deleted the directory and
 
 
473
    the OTHER added a file to it.
 
 
474
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
 
477
    typestring = 'missing parent'
 
 
479
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
 
482
class DeletingParent(HandledConflict):
 
 
483
    """An attempt to add files to a directory that is not present.
 
 
484
    Typically, the result of a merge where one OTHER deleted the directory and
 
 
485
    the THIS added a file to it.
 
 
488
    typestring = 'deleting parent'
 
 
490
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
 
497
def register_types(*conflict_types):
 
 
498
    """Register a Conflict subclass for serialization purposes"""
 
 
500
    for conflict_type in conflict_types:
 
 
501
        ctype[conflict_type.typestring] = conflict_type
 
 
504
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
 
505
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,