/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: Martin Pool
  • Date: 2007-07-10 10:20:27 UTC
  • mto: This revision was merged to the branch mainline in revision 2599.
  • Revision ID: mbp@sourcefrog.net-20070710102027-2os88re33c57m522
Add test for and documentation of option style, fix up existing options to comply

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