~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Robert Collins
  • Date: 2005-10-18 05:26:22 UTC
  • mto: This revision was merged to the branch mainline in revision 1463.
  • Revision ID: robertc@robertcollins.net-20051018052622-653d638c9e26fde4
fix broken tests

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
 
22
25
import os
23
26
import errno
24
27
 
25
 
import bzrlib
 
28
import bzrlib.status
 
29
from bzrlib.branch import Branch
 
30
from bzrlib.errors import BzrCommandError
26
31
from bzrlib.commands import register_command
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
 
 
 
32
from bzrlib.workingtree import CONFLICT_SUFFIXES
35
33
 
36
34
class cmd_conflicts(bzrlib.commands.Command):
37
35
    """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
 
 
46
36
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
47
37
    files.)
48
 
 
49
 
    See also bzr resolve.
50
38
    """
51
39
    def run(self):
52
 
        from bzrlib.workingtree import WorkingTree
53
 
        wt = WorkingTree.open_containing(u'.')[0]
54
 
        for conflict in wt.conflicts():
55
 
            print conflict
56
 
 
 
40
        for path in Branch.open_containing('.')[0].working_tree().iter_conflicts():
 
41
            print path
57
42
 
58
43
class cmd_resolve(bzrlib.commands.Command):
59
44
    """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.
71
45
    """
72
 
    aliases = ['resolved']
73
46
    takes_args = ['file*']
74
 
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
 
47
    takes_options = ['all']
75
48
    def run(self, file_list=None, all=False):
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
 
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
199
61
            for suffix in CONFLICT_SUFFIXES:
200
62
                try:
201
 
                    delete_any(tree.abspath(conflict.path+suffix))
 
63
                    os.unlink(filename+suffix)
202
64
                except OSError, e:
203
65
                    if e.errno != errno.ENOENT:
204
66
                        raise
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
 
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
248
72
                else:
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,)
 
73
                    print "%s is not conflicted" % filename