~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Martin Pool
  • Date: 2005-06-22 06:37:43 UTC
  • Revision ID: mbp@sourcefrog.net-20050622063743-e395f04c4db8977f
- move old blackbox code from testbzr into bzrlib.selftest.blackbox

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