~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

Late bind to PatienceSequenceMatcher to allow plugin to override.

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