/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Robert Collins
  • Date: 2006-11-08 00:36:30 UTC
  • mto: This revision was merged to the branch mainline in revision 2124.
  • Revision ID: robertc@robertcollins.net-20061108003630-feb31613c83f7096
(Robert Collins) Extend the problem reporting command line UI to use
apport to report more detailed diagnostics which should help in in getting
faults reported in Malone and provides the basis for capturing more
information such as detailed logging data from the current invocation of
bzr in the future (without cluttering 'bzr.log' unnecessarily).
apport is available from Ubuntu Edgy onwards.

Show diffs side-by-side

added added

removed removed

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