/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-03-03 02:09:49 UTC
  • mto: (1594.2.4 integration)
  • mto: This revision was merged to the branch mainline in revision 1596.
  • Revision ID: robertc@robertcollins.net-20060303020949-0ddc6f33d0a43943
Smoke test for RevisionStore factories creating revision stores.

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, NotConflicted
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
 
33
from bzrlib.osutils import rename
35
34
 
36
35
class cmd_conflicts(bzrlib.commands.Command):
37
36
    """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
37
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
47
38
    files.)
48
 
 
49
 
    See also bzr resolve.
50
39
    """
51
40
    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
 
 
 
41
        for path in Branch.open_containing(u'.')[0].working_tree().iter_conflicts():
 
42
            print path
57
43
 
58
44
class cmd_resolve(bzrlib.commands.Command):
59
45
    """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
46
    """
72
47
    aliases = ['resolved']
73
48
    takes_args = ['file*']
74
 
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
 
49
    takes_options = ['all']
75
50
    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
 
 
 
51
        if file_list is None:
 
52
            if not all:
 
53
                raise BzrCommandError(
 
54
                    "command 'resolve' needs one or more FILE, or --all")
 
55
            tree = Branch.open_containing(u'.')[0].working_tree()
 
56
            file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
 
57
        else:
 
58
            if all:
 
59
                raise BzrCommandError(
 
60
                    "If --all is specified, no FILE may be provided")
 
61
        for filename in file_list:
 
62
            failures = 0
 
63
            for suffix in CONFLICT_SUFFIXES:
 
64
                try:
 
65
                    os.unlink(filename+suffix)
 
66
                except OSError, e:
 
67
                    if e.errno != errno.ENOENT:
 
68
                        raise
 
69
                    else:
 
70
                        failures += 1
 
71
            if failures == len(CONFLICT_SUFFIXES):
 
72
                if not os.path.exists(filename):
 
73
                    print "%s does not exist" % filename
 
74
                else:
 
75
                    print "%s is not conflicted" % filename
108
76
 
109
77
def restore(filename):
110
78
    """\
132
100
            raise
133
101
    if not conflicted:
134
102
        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
199
 
            for suffix in CONFLICT_SUFFIXES:
200
 
                try:
201
 
                    delete_any(tree.abspath(conflict.path+suffix))
202
 
                except OSError, e:
203
 
                    if e.errno != errno.ENOENT:
204
 
                        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
248
 
                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
 
 
296
 
class PathConflict(Conflict):
297
 
    """A conflict was encountered merging file paths"""
298
 
 
299
 
    typestring = 'path conflict'
300
 
 
301
 
    format = 'Path conflict: %(path)s / %(conflict_path)s'
302
 
 
303
 
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
304
 
    def __init__(self, path, conflict_path=None, file_id=None):
305
 
        Conflict.__init__(self, path, file_id)
306
 
        self.conflict_path = conflict_path
307
 
 
308
 
    def as_stanza(self):
309
 
        s = Conflict.as_stanza(self)
310
 
        if self.conflict_path is not None:
311
 
            s.add('conflict_path', self.conflict_path)
312
 
        return s
313
 
 
314
 
 
315
 
class ContentsConflict(PathConflict):
316
 
    """The files are of different types, or not present"""
317
 
 
318
 
    has_files = True
319
 
 
320
 
    typestring = 'contents conflict'
321
 
 
322
 
    format = 'Contents conflict in %(path)s'
323
 
 
324
 
 
325
 
class TextConflict(PathConflict):
326
 
    """The merge algorithm could not resolve all differences encountered."""
327
 
 
328
 
    has_files = True
329
 
 
330
 
    typestring = 'text conflict'
331
 
 
332
 
    format = 'Text conflict in %(path)s'
333
 
 
334
 
 
335
 
class HandledConflict(Conflict):
336
 
    """A path problem that has been provisionally resolved.
337
 
    This is intended to be a base class.
338
 
    """
339
 
 
340
 
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
341
 
    
342
 
    def __init__(self, action, path, file_id=None):
343
 
        Conflict.__init__(self, path, file_id)
344
 
        self.action = action
345
 
 
346
 
    def _cmp_list(self):
347
 
        return Conflict._cmp_list(self) + [self.action]
348
 
 
349
 
    def as_stanza(self):
350
 
        s = Conflict.as_stanza(self)
351
 
        s.add('action', self.action)
352
 
        return s
353
 
 
354
 
 
355
 
class HandledPathConflict(HandledConflict):
356
 
    """A provisionally-resolved path problem involving two paths.
357
 
    This is intended to be a base class.
358
 
    """
359
 
 
360
 
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
361
 
        " %(file_id)r, %(conflict_file_id)r)"
362
 
 
363
 
    def __init__(self, action, path, conflict_path, file_id=None,
364
 
                 conflict_file_id=None):
365
 
        HandledConflict.__init__(self, action, path, file_id)
366
 
        self.conflict_path = conflict_path 
367
 
        self.conflict_file_id = conflict_file_id
368
 
        
369
 
    def _cmp_list(self):
370
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
371
 
                                                  self.conflict_file_id]
372
 
 
373
 
    def as_stanza(self):
374
 
        s = HandledConflict.as_stanza(self)
375
 
        s.add('conflict_path', self.conflict_path)
376
 
        if self.conflict_file_id is not None:
377
 
            s.add('conflict_file_id', self.conflict_file_id)
378
 
            
379
 
        return s
380
 
 
381
 
 
382
 
class DuplicateID(HandledPathConflict):
383
 
    """Two files want the same file_id."""
384
 
 
385
 
    typestring = 'duplicate id'
386
 
 
387
 
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
388
 
 
389
 
 
390
 
class DuplicateEntry(HandledPathConflict):
391
 
    """Two directory entries want to have the same name."""
392
 
 
393
 
    typestring = 'duplicate'
394
 
 
395
 
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
396
 
 
397
 
 
398
 
class ParentLoop(HandledPathConflict):
399
 
    """An attempt to create an infinitely-looping directory structure.
400
 
    This is rare, but can be produced like so:
401
 
 
402
 
    tree A:
403
 
      mv foo/bar
404
 
    tree B:
405
 
      mv bar/foo
406
 
    merge A and B
407
 
    """
408
 
 
409
 
    typestring = 'parent loop'
410
 
 
411
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
412
 
 
413
 
 
414
 
class UnversionedParent(HandledConflict):
415
 
    """An attempt to version an file whose parent directory is not versioned.
416
 
    Typically, the result of a merge where one tree unversioned the directory
417
 
    and the other added a versioned file to it.
418
 
    """
419
 
 
420
 
    typestring = 'unversioned parent'
421
 
 
422
 
    format = 'Conflict adding versioned files to %(path)s.  %(action)s.'
423
 
 
424
 
 
425
 
class MissingParent(HandledConflict):
426
 
    """An attempt to add files to a directory that is not present.
427
 
    Typically, the result of a merge where one tree deleted the directory and
428
 
    the other added a file to it.
429
 
    """
430
 
 
431
 
    typestring = 'missing parent'
432
 
 
433
 
    format = 'Conflict adding files to %(path)s.  %(action)s.'
434
 
 
435
 
 
436
 
 
437
 
ctype = {}
438
 
 
439
 
 
440
 
def register_types(*conflict_types):
441
 
    """Register a Conflict subclass for serialization purposes"""
442
 
    global ctype
443
 
    for conflict_type in conflict_types:
444
 
        ctype[conflict_type.typestring] = conflict_type
445
 
 
446
 
 
447
 
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
448
 
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)