~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-01-21 10:00:42 UTC
  • mfrom: (4978.1.2 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20100121100042-77858a88k2zpec7b
(nmb) Add documentation for multi-parent merges

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)