~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-04-09 16:49:22 UTC
  • mfrom: (1534.10.27 bzr.ttransform)
  • Revision ID: pqm@pqm.ubuntu.com-20060409164922-071803e906c8b96d
New conflict-handling system

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
 
25
22
import os
26
23
import errno
27
24
 
28
 
import bzrlib.status
 
25
import bzrlib
29
26
from bzrlib.commands import register_command
30
 
from bzrlib.errors import BzrCommandError, NotConflicted
 
27
from bzrlib.errors import BzrCommandError, NotConflicted, UnsupportedOperation
31
28
from bzrlib.option import Option
32
 
from bzrlib.workingtree import CONFLICT_SUFFIXES, WorkingTree
33
29
from bzrlib.osutils import rename
 
30
from bzrlib.rio import Stanza
 
31
 
 
32
 
 
33
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
34
 
34
35
 
35
36
class cmd_conflicts(bzrlib.commands.Command):
36
37
    """List files with conflicts.
48
49
    See also bzr resolve.
49
50
    """
50
51
    def run(self):
51
 
        for path in WorkingTree.open_containing(u'.')[0].iter_conflicts():
52
 
            print path
 
52
        from bzrlib.workingtree import WorkingTree
 
53
        wt = WorkingTree.open_containing(u'.')[0]
 
54
        for conflict in wt.conflicts():
 
55
            print conflict
53
56
 
54
57
class cmd_resolve(bzrlib.commands.Command):
55
58
    """Mark a conflict as resolved.
69
72
    takes_args = ['file*']
70
73
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
71
74
    def run(self, file_list=None, all=False):
 
75
        from bzrlib.workingtree import WorkingTree
72
76
        if file_list is None:
73
77
            if not all:
74
78
                raise BzrCommandError(
75
79
                    "command 'resolve' needs one or more FILE, or --all")
76
 
            tree = WorkingTree.open_containing(u'.')[0]
77
 
            file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
78
80
        else:
79
81
            if all:
80
82
                raise BzrCommandError(
81
83
                    "If --all is specified, no FILE may be provided")
82
 
        for filename in file_list:
83
 
            failures = 0
84
 
            for suffix in CONFLICT_SUFFIXES:
85
 
                try:
86
 
                    os.unlink(filename+suffix)
87
 
                except OSError, e:
88
 
                    if e.errno != errno.ENOENT:
89
 
                        raise
90
 
                    else:
91
 
                        failures += 1
92
 
            if failures == len(CONFLICT_SUFFIXES):
93
 
                if not os.path.exists(filename):
94
 
                    print "%s does not exist" % filename
95
 
                else:
96
 
                    print "%s is not conflicted" % filename
 
84
        tree = WorkingTree.open_containing(u'.')[0]
 
85
        resolve(tree, file_list)
 
86
 
 
87
 
 
88
def resolve(tree, paths=None, ignore_misses=False):
 
89
    tree.lock_write()
 
90
    try:
 
91
        tree_conflicts = tree.conflicts()
 
92
        if paths is None:
 
93
            new_conflicts = ConflictList()
 
94
            selected_conflicts = tree_conflicts
 
95
        else:
 
96
            new_conflicts, selected_conflicts = \
 
97
                tree_conflicts.select_conflicts(tree, paths, ignore_misses)
 
98
        try:
 
99
            tree.set_conflicts(new_conflicts)
 
100
        except UnsupportedOperation:
 
101
            pass
 
102
        selected_conflicts.remove_files(tree)
 
103
    finally:
 
104
        tree.unlock()
 
105
 
97
106
 
98
107
def restore(filename):
99
108
    """\
121
130
            raise
122
131
    if not conflicted:
123
132
        raise NotConflicted(filename)
 
133
 
 
134
 
 
135
class ConflictList(object):
 
136
    """List of conflicts
 
137
    Can be instantiated from stanzas or from Conflict subclasses.
 
138
    """
 
139
 
 
140
    def __init__(self, conflicts=None):
 
141
        object.__init__(self)
 
142
        if conflicts is None:
 
143
            self.__list = []
 
144
        else:
 
145
            self.__list = conflicts
 
146
 
 
147
    def __len__(self):
 
148
        return len(self.__list)
 
149
 
 
150
    def __iter__(self):
 
151
        return iter(self.__list)
 
152
 
 
153
    def __getitem__(self, key):
 
154
        return self.__list[key]
 
155
 
 
156
    def append(self, conflict):
 
157
        return self.__list.append(conflict)
 
158
 
 
159
    def __eq__(self, other_list):
 
160
        return list(self) == list(other_list)
 
161
 
 
162
    def __ne__(self, other_list):
 
163
        return not (self == other_list)
 
164
 
 
165
    def __repr__(self):
 
166
        return "ConflictList(%r)" % self.__list
 
167
 
 
168
    @staticmethod
 
169
    def from_stanzas(stanzas):
 
170
        """Produce a new ConflictList from an iterable of stanzas"""
 
171
        conflicts = ConflictList()
 
172
        for stanza in stanzas:
 
173
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
174
        return conflicts
 
175
 
 
176
    def to_stanzas(self):
 
177
        """Generator of stanzas"""
 
178
        for conflict in self:
 
179
            yield conflict.as_stanza()
 
180
            
 
181
    def to_strings(self):
 
182
        """Generate strings for the provided conflicts"""
 
183
        for conflict in self:
 
184
            yield str(conflict)
 
185
 
 
186
    def remove_files(self, tree):
 
187
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
 
188
        for conflict in self:
 
189
            if not conflict.has_files:
 
190
                continue
 
191
            for suffix in CONFLICT_SUFFIXES:
 
192
                try:
 
193
                    os.unlink(tree.abspath(conflict.path+suffix))
 
194
                except OSError, e:
 
195
                    if e.errno != errno.ENOENT:
 
196
                        raise
 
197
 
 
198
    def select_conflicts(self, tree, paths, ignore_misses=False):
 
199
        """Select the conflicts associated with paths in a tree.
 
200
        
 
201
        File-ids are also used for this.
 
202
        """
 
203
        path_set = set(paths)
 
204
        ids = {}
 
205
        selected_paths = set()
 
206
        new_conflicts = ConflictList()
 
207
        selected_conflicts = ConflictList()
 
208
        for path in paths:
 
209
            file_id = tree.path2id(path)
 
210
            if file_id is not None:
 
211
                ids[file_id] = path
 
212
 
 
213
        for conflict in self:
 
214
            selected = False
 
215
            for key in ('path', 'conflict_path'):
 
216
                cpath = getattr(conflict, key, None)
 
217
                if cpath is None:
 
218
                    continue
 
219
                if cpath in path_set:
 
220
                    selected = True
 
221
                    selected_paths.add(cpath)
 
222
            for key in ('file_id', 'conflict_file_id'):
 
223
                cfile_id = getattr(conflict, key, None)
 
224
                if cfile_id is None:
 
225
                    continue
 
226
                try:
 
227
                    cpath = ids[cfile_id]
 
228
                except KeyError:
 
229
                    continue
 
230
                selected = True
 
231
                selected_paths.add(cpath)
 
232
            if selected:
 
233
                selected_conflicts.append(conflict)
 
234
            else:
 
235
                new_conflicts.append(conflict)
 
236
        if ignore_misses is not True:
 
237
            for path in [p for p in paths if p not in selected_paths]:
 
238
                if not os.path.exists(tree.abspath(path)):
 
239
                    print "%s does not exist" % path
 
240
                else:
 
241
                    print "%s is not conflicted" % path
 
242
        return new_conflicts, selected_conflicts
 
243
 
 
244
 
 
245
class Conflict(object):
 
246
    """Base class for all types of conflict"""
 
247
 
 
248
    has_files = False
 
249
 
 
250
    def __init__(self, path, file_id=None):
 
251
        self.path = path
 
252
        self.file_id = file_id
 
253
 
 
254
    def as_stanza(self):
 
255
        s = Stanza(type=self.typestring, path=self.path)
 
256
        if self.file_id is not None:
 
257
            s.add('file_id', self.file_id)
 
258
        return s
 
259
 
 
260
    def _cmp_list(self):
 
261
        return [type(self), self.path, self.file_id]
 
262
 
 
263
    def __cmp__(self, other):
 
264
        if getattr(other, "_cmp_list", None) is None:
 
265
            return -1
 
266
        return cmp(self._cmp_list(), other._cmp_list())
 
267
 
 
268
    def __eq__(self, other):
 
269
        return self.__cmp__(other) == 0
 
270
 
 
271
    def __ne__(self, other):
 
272
        return not self.__eq__(other)
 
273
 
 
274
    def __str__(self):
 
275
        return self.format % self.__dict__
 
276
 
 
277
    def __repr__(self):
 
278
        rdict = dict(self.__dict__)
 
279
        rdict['class'] = self.__class__.__name__
 
280
        return self.rformat % rdict
 
281
 
 
282
    @staticmethod
 
283
    def factory(type, **kwargs):
 
284
        global ctype
 
285
        return ctype[type](**kwargs)
 
286
 
 
287
 
 
288
class PathConflict(Conflict):
 
289
    """A conflict was encountered merging file paths"""
 
290
 
 
291
    typestring = 'path conflict'
 
292
 
 
293
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
294
 
 
295
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
296
    def __init__(self, path, conflict_path=None, file_id=None):
 
297
        Conflict.__init__(self, path, file_id)
 
298
        self.conflict_path = conflict_path
 
299
 
 
300
    def as_stanza(self):
 
301
        s = Conflict.as_stanza(self)
 
302
        if self.conflict_path is not None:
 
303
            s.add('conflict_path', self.conflict_path)
 
304
        return s
 
305
 
 
306
 
 
307
class ContentsConflict(PathConflict):
 
308
    """The files are of different types, or not present"""
 
309
 
 
310
    has_files = True
 
311
 
 
312
    typestring = 'contents conflict'
 
313
 
 
314
    format = 'Contents conflict in %(path)s'
 
315
 
 
316
 
 
317
class TextConflict(PathConflict):
 
318
    """The merge algorithm could not resolve all differences encountered."""
 
319
 
 
320
    has_files = True
 
321
 
 
322
    typestring = 'text conflict'
 
323
 
 
324
    format = 'Text conflict in %(path)s'
 
325
 
 
326
 
 
327
class HandledConflict(Conflict):
 
328
    """A path problem that has been provisionally resolved.
 
329
    This is intended to be a base class.
 
330
    """
 
331
 
 
332
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
333
    
 
334
    def __init__(self, action, path, file_id=None):
 
335
        Conflict.__init__(self, path, file_id)
 
336
        self.action = action
 
337
 
 
338
    def _cmp_list(self):
 
339
        return Conflict._cmp_list(self) + [self.action]
 
340
 
 
341
    def as_stanza(self):
 
342
        s = Conflict.as_stanza(self)
 
343
        s.add('action', self.action)
 
344
        return s
 
345
 
 
346
 
 
347
class HandledPathConflict(HandledConflict):
 
348
    """A provisionally-resolved path problem involving two paths.
 
349
    This is intended to be a base class.
 
350
    """
 
351
 
 
352
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
353
        " %(file_id)r, %(conflict_file_id)r)"
 
354
 
 
355
    def __init__(self, action, path, conflict_path, file_id=None,
 
356
                 conflict_file_id=None):
 
357
        HandledConflict.__init__(self, action, path, file_id)
 
358
        self.conflict_path = conflict_path 
 
359
        self.conflict_file_id = conflict_file_id
 
360
        
 
361
    def _cmp_list(self):
 
362
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
363
                                                  self.conflict_file_id]
 
364
 
 
365
    def as_stanza(self):
 
366
        s = HandledConflict.as_stanza(self)
 
367
        s.add('conflict_path', self.conflict_path)
 
368
        if self.conflict_file_id is not None:
 
369
            s.add('conflict_file_id', self.conflict_file_id)
 
370
            
 
371
        return s
 
372
 
 
373
 
 
374
class DuplicateID(HandledPathConflict):
 
375
    """Two files want the same file_id."""
 
376
 
 
377
    typestring = 'duplicate id'
 
378
 
 
379
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
380
 
 
381
 
 
382
class DuplicateEntry(HandledPathConflict):
 
383
    """Two directory entries want to have the same name."""
 
384
 
 
385
    typestring = 'duplicate'
 
386
 
 
387
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
388
 
 
389
 
 
390
class ParentLoop(HandledPathConflict):
 
391
    """An attempt to create an infinitely-looping directory structure.
 
392
    This is rare, but can be produced like so:
 
393
 
 
394
    tree A:
 
395
      mv foo/bar
 
396
    tree B:
 
397
      mv bar/foo
 
398
    merge A and B
 
399
    """
 
400
 
 
401
    typestring = 'parent loop'
 
402
 
 
403
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
404
 
 
405
 
 
406
class UnversionedParent(HandledConflict):
 
407
    """An attempt to version an file whose parent directory is not versioned.
 
408
    Typically, the result of a merge where one tree unversioned the directory
 
409
    and the other added a versioned file to it.
 
410
    """
 
411
 
 
412
    typestring = 'unversioned parent'
 
413
 
 
414
    format = 'Conflict adding versioned files to %(path)s.  %(action)s.'
 
415
 
 
416
 
 
417
class MissingParent(HandledConflict):
 
418
    """An attempt to add files to a directory that is not present.
 
419
    Typically, the result of a merge where one tree deleted the directory and
 
420
    the other added a file to it.
 
421
    """
 
422
 
 
423
    typestring = 'missing parent'
 
424
 
 
425
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
426
 
 
427
 
 
428
 
 
429
ctype = {}
 
430
 
 
431
 
 
432
def register_types(*conflict_types):
 
433
    """Register a Conflict subclass for serialization purposes"""
 
434
    global ctype
 
435
    for conflict_type in conflict_types:
 
436
        ctype[conflict_type.typestring] = conflict_type
 
437
 
 
438
 
 
439
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
440
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)