~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Martin Pool
  • Date: 2005-05-05 07:00:55 UTC
  • Revision ID: mbp@sourcefrog.net-20050505070055-e1ef8f7dd14b48b1
- Fix up bzr log command

Show diffs side-by-side

added added

removed removed

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