~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Aaron Bentley
2
 
 
 
1
# Copyright (C) 2005, 2007 Canonical Ltd
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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
 
 
25
22
import os
 
23
 
 
24
from bzrlib.lazy_import import lazy_import
 
25
lazy_import(globals(), """
26
26
import errno
27
27
 
28
 
import bzrlib.status
29
 
from bzrlib.branch import Branch
30
 
from bzrlib.errors import BzrCommandError, NotConflicted
31
 
from bzrlib.commands import register_command
32
 
from bzrlib.workingtree import CONFLICT_SUFFIXES
33
 
from bzrlib.osutils import rename
34
 
 
35
 
class cmd_conflicts(bzrlib.commands.Command):
 
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):
36
44
    """List files with conflicts.
37
 
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
38
 
    files.)
 
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.
39
58
    """
40
 
    def run(self):
41
 
        for path in Branch.open_containing(u'.')[0].working_tree().iter_conflicts():
42
 
            print path
43
 
 
44
 
class cmd_resolve(bzrlib.commands.Command):
 
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):
45
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.
46
89
    """
47
90
    aliases = ['resolved']
48
91
    takes_args = ['file*']
49
 
    takes_options = ['all']
 
92
    takes_options = [
 
93
            Option('all', help='Resolve all conflicts in this tree.'),
 
94
            ]
50
95
    def run(self, file_list=None, all=False):
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())
 
96
        from bzrlib.workingtree import WorkingTree
 
97
        if all:
 
98
            if file_list:
 
99
                raise errors.BzrCommandError("If --all is specified,"
 
100
                                             " no FILE may be provided")
 
101
            tree = WorkingTree.open_containing('.')[0]
 
102
            resolve(tree)
57
103
        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
 
104
            tree, file_list = builtins.tree_files(file_list)
 
105
            if file_list is None:
 
106
                un_resolved, resolved = tree.auto_resolve()
 
107
                if len(un_resolved) > 0:
 
108
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
 
109
                    trace.note('Remaining conflicts:')
 
110
                    for conflict in un_resolved:
 
111
                        trace.note(conflict)
 
112
                    return 1
74
113
                else:
75
 
                    print "%s is not conflicted" % filename
 
114
                    trace.note('All conflicts resolved.')
 
115
                    return 0
 
116
            else:
 
117
                resolve(tree, file_list)
 
118
 
 
119
 
 
120
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
 
121
    """Resolve some or all of the conflicts in a working tree.
 
122
 
 
123
    :param paths: If None, resolve all conflicts.  Otherwise, select only
 
124
        specified conflicts.
 
125
    :param recursive: If True, then elements of paths which are directories
 
126
        have all their children resolved, etc.  When invoked as part of
 
127
        recursive commands like revert, this should be True.  For commands
 
128
        or applications wishing finer-grained control, like the resolve
 
129
        command, this should be False.
 
130
    :ignore_misses: If False, warnings will be printed if the supplied paths
 
131
        do not have conflicts.
 
132
    """
 
133
    tree.lock_tree_write()
 
134
    try:
 
135
        tree_conflicts = tree.conflicts()
 
136
        if paths is None:
 
137
            new_conflicts = ConflictList()
 
138
            selected_conflicts = tree_conflicts
 
139
        else:
 
140
            new_conflicts, selected_conflicts = \
 
141
                tree_conflicts.select_conflicts(tree, paths, ignore_misses,
 
142
                    recursive)
 
143
        try:
 
144
            tree.set_conflicts(new_conflicts)
 
145
        except errors.UnsupportedOperation:
 
146
            pass
 
147
        selected_conflicts.remove_files(tree)
 
148
    finally:
 
149
        tree.unlock()
 
150
 
76
151
 
77
152
def restore(filename):
78
153
    """\
81
156
    """
82
157
    conflicted = False
83
158
    try:
84
 
        rename(filename + ".THIS", filename)
 
159
        osutils.rename(filename + ".THIS", filename)
85
160
        conflicted = True
86
161
    except OSError, e:
87
162
        if e.errno != errno.ENOENT:
99
174
        if e.errno != errno.ENOENT:
100
175
            raise
101
176
    if not conflicted:
102
 
        raise NotConflicted(filename)
 
177
        raise errors.NotConflicted(filename)
 
178
 
 
179
 
 
180
class ConflictList(object):
 
181
    """List of conflicts.
 
182
 
 
183
    Typically obtained from WorkingTree.conflicts()
 
184
 
 
185
    Can be instantiated from stanzas or from Conflict subclasses.
 
186
    """
 
187
 
 
188
    def __init__(self, conflicts=None):
 
189
        object.__init__(self)
 
190
        if conflicts is None:
 
191
            self.__list = []
 
192
        else:
 
193
            self.__list = conflicts
 
194
 
 
195
    def is_empty(self):
 
196
        return len(self.__list) == 0
 
197
 
 
198
    def __len__(self):
 
199
        return len(self.__list)
 
200
 
 
201
    def __iter__(self):
 
202
        return iter(self.__list)
 
203
 
 
204
    def __getitem__(self, key):
 
205
        return self.__list[key]
 
206
 
 
207
    def append(self, conflict):
 
208
        return self.__list.append(conflict)
 
209
 
 
210
    def __eq__(self, other_list):
 
211
        return list(self) == list(other_list)
 
212
 
 
213
    def __ne__(self, other_list):
 
214
        return not (self == other_list)
 
215
 
 
216
    def __repr__(self):
 
217
        return "ConflictList(%r)" % self.__list
 
218
 
 
219
    @staticmethod
 
220
    def from_stanzas(stanzas):
 
221
        """Produce a new ConflictList from an iterable of stanzas"""
 
222
        conflicts = ConflictList()
 
223
        for stanza in stanzas:
 
224
            conflicts.append(Conflict.factory(**stanza.as_dict()))
 
225
        return conflicts
 
226
 
 
227
    def to_stanzas(self):
 
228
        """Generator of stanzas"""
 
229
        for conflict in self:
 
230
            yield conflict.as_stanza()
 
231
            
 
232
    def to_strings(self):
 
233
        """Generate strings for the provided conflicts"""
 
234
        for conflict in self:
 
235
            yield str(conflict)
 
236
 
 
237
    def remove_files(self, tree):
 
238
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
 
239
        for conflict in self:
 
240
            if not conflict.has_files:
 
241
                continue
 
242
            for suffix in CONFLICT_SUFFIXES:
 
243
                try:
 
244
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
 
245
                except OSError, e:
 
246
                    if e.errno != errno.ENOENT:
 
247
                        raise
 
248
 
 
249
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
250
                         recurse=False):
 
251
        """Select the conflicts associated with paths in a tree.
 
252
        
 
253
        File-ids are also used for this.
 
254
        :return: a pair of ConflictLists: (not_selected, selected)
 
255
        """
 
256
        path_set = set(paths)
 
257
        ids = {}
 
258
        selected_paths = set()
 
259
        new_conflicts = ConflictList()
 
260
        selected_conflicts = ConflictList()
 
261
        for path in paths:
 
262
            file_id = tree.path2id(path)
 
263
            if file_id is not None:
 
264
                ids[file_id] = path
 
265
 
 
266
        for conflict in self:
 
267
            selected = False
 
268
            for key in ('path', 'conflict_path'):
 
269
                cpath = getattr(conflict, key, None)
 
270
                if cpath is None:
 
271
                    continue
 
272
                if cpath in path_set:
 
273
                    selected = True
 
274
                    selected_paths.add(cpath)
 
275
                if recurse:
 
276
                    if osutils.is_inside_any(path_set, cpath):
 
277
                        selected = True
 
278
                        selected_paths.add(cpath)
 
279
 
 
280
            for key in ('file_id', 'conflict_file_id'):
 
281
                cfile_id = getattr(conflict, key, None)
 
282
                if cfile_id is None:
 
283
                    continue
 
284
                try:
 
285
                    cpath = ids[cfile_id]
 
286
                except KeyError:
 
287
                    continue
 
288
                selected = True
 
289
                selected_paths.add(cpath)
 
290
            if selected:
 
291
                selected_conflicts.append(conflict)
 
292
            else:
 
293
                new_conflicts.append(conflict)
 
294
        if ignore_misses is not True:
 
295
            for path in [p for p in paths if p not in selected_paths]:
 
296
                if not os.path.exists(tree.abspath(path)):
 
297
                    print "%s does not exist" % path
 
298
                else:
 
299
                    print "%s is not conflicted" % path
 
300
        return new_conflicts, selected_conflicts
 
301
 
 
302
 
 
303
class Conflict(object):
 
304
    """Base class for all types of conflict"""
 
305
 
 
306
    has_files = False
 
307
 
 
308
    def __init__(self, path, file_id=None):
 
309
        self.path = path
 
310
        # warn turned off, because the factory blindly transfers the Stanza
 
311
        # values to __init__ and Stanza is purely a Unicode api.
 
312
        self.file_id = osutils.safe_file_id(file_id, warn=False)
 
313
 
 
314
    def as_stanza(self):
 
315
        s = rio.Stanza(type=self.typestring, path=self.path)
 
316
        if self.file_id is not None:
 
317
            # Stanza requires Unicode apis
 
318
            s.add('file_id', self.file_id.decode('utf8'))
 
319
        return s
 
320
 
 
321
    def _cmp_list(self):
 
322
        return [type(self), self.path, self.file_id]
 
323
 
 
324
    def __cmp__(self, other):
 
325
        if getattr(other, "_cmp_list", None) is None:
 
326
            return -1
 
327
        return cmp(self._cmp_list(), other._cmp_list())
 
328
 
 
329
    def __hash__(self):
 
330
        return hash((type(self), self.path, self.file_id))
 
331
 
 
332
    def __eq__(self, other):
 
333
        return self.__cmp__(other) == 0
 
334
 
 
335
    def __ne__(self, other):
 
336
        return not self.__eq__(other)
 
337
 
 
338
    def __str__(self):
 
339
        return self.format % self.__dict__
 
340
 
 
341
    def __repr__(self):
 
342
        rdict = dict(self.__dict__)
 
343
        rdict['class'] = self.__class__.__name__
 
344
        return self.rformat % rdict
 
345
 
 
346
    @staticmethod
 
347
    def factory(type, **kwargs):
 
348
        global ctype
 
349
        return ctype[type](**kwargs)
 
350
 
 
351
    @staticmethod
 
352
    def sort_key(conflict):
 
353
        if conflict.path is not None:
 
354
            return conflict.path, conflict.typestring
 
355
        elif getattr(conflict, "conflict_path", None) is not None:
 
356
            return conflict.conflict_path, conflict.typestring
 
357
        else:
 
358
            return None, conflict.typestring
 
359
 
 
360
 
 
361
class PathConflict(Conflict):
 
362
    """A conflict was encountered merging file paths"""
 
363
 
 
364
    typestring = 'path conflict'
 
365
 
 
366
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
367
 
 
368
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
369
    def __init__(self, path, conflict_path=None, file_id=None):
 
370
        Conflict.__init__(self, path, file_id)
 
371
        self.conflict_path = conflict_path
 
372
 
 
373
    def as_stanza(self):
 
374
        s = Conflict.as_stanza(self)
 
375
        if self.conflict_path is not None:
 
376
            s.add('conflict_path', self.conflict_path)
 
377
        return s
 
378
 
 
379
 
 
380
class ContentsConflict(PathConflict):
 
381
    """The files are of different types, or not present"""
 
382
 
 
383
    has_files = True
 
384
 
 
385
    typestring = 'contents conflict'
 
386
 
 
387
    format = 'Contents conflict in %(path)s'
 
388
 
 
389
 
 
390
class TextConflict(PathConflict):
 
391
    """The merge algorithm could not resolve all differences encountered."""
 
392
 
 
393
    has_files = True
 
394
 
 
395
    typestring = 'text conflict'
 
396
 
 
397
    format = 'Text conflict in %(path)s'
 
398
 
 
399
 
 
400
class HandledConflict(Conflict):
 
401
    """A path problem that has been provisionally resolved.
 
402
    This is intended to be a base class.
 
403
    """
 
404
 
 
405
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
406
    
 
407
    def __init__(self, action, path, file_id=None):
 
408
        Conflict.__init__(self, path, file_id)
 
409
        self.action = action
 
410
 
 
411
    def _cmp_list(self):
 
412
        return Conflict._cmp_list(self) + [self.action]
 
413
 
 
414
    def as_stanza(self):
 
415
        s = Conflict.as_stanza(self)
 
416
        s.add('action', self.action)
 
417
        return s
 
418
 
 
419
 
 
420
class HandledPathConflict(HandledConflict):
 
421
    """A provisionally-resolved path problem involving two paths.
 
422
    This is intended to be a base class.
 
423
    """
 
424
 
 
425
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
426
        " %(file_id)r, %(conflict_file_id)r)"
 
427
 
 
428
    def __init__(self, action, path, conflict_path, file_id=None,
 
429
                 conflict_file_id=None):
 
430
        HandledConflict.__init__(self, action, path, file_id)
 
431
        self.conflict_path = conflict_path 
 
432
        # warn turned off, because the factory blindly transfers the Stanza
 
433
        # values to __init__.
 
434
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
 
435
                                                     warn=False)
 
436
        
 
437
    def _cmp_list(self):
 
438
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
439
                                                  self.conflict_file_id]
 
440
 
 
441
    def as_stanza(self):
 
442
        s = HandledConflict.as_stanza(self)
 
443
        s.add('conflict_path', self.conflict_path)
 
444
        if self.conflict_file_id is not None:
 
445
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
446
            
 
447
        return s
 
448
 
 
449
 
 
450
class DuplicateID(HandledPathConflict):
 
451
    """Two files want the same file_id."""
 
452
 
 
453
    typestring = 'duplicate id'
 
454
 
 
455
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
456
 
 
457
 
 
458
class DuplicateEntry(HandledPathConflict):
 
459
    """Two directory entries want to have the same name."""
 
460
 
 
461
    typestring = 'duplicate'
 
462
 
 
463
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
464
 
 
465
 
 
466
class ParentLoop(HandledPathConflict):
 
467
    """An attempt to create an infinitely-looping directory structure.
 
468
    This is rare, but can be produced like so:
 
469
 
 
470
    tree A:
 
471
      mv foo/bar
 
472
    tree B:
 
473
      mv bar/foo
 
474
    merge A and B
 
475
    """
 
476
 
 
477
    typestring = 'parent loop'
 
478
 
 
479
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
480
 
 
481
 
 
482
class UnversionedParent(HandledConflict):
 
483
    """An attempt to version an file whose parent directory is not versioned.
 
484
    Typically, the result of a merge where one tree unversioned the directory
 
485
    and the other added a versioned file to it.
 
486
    """
 
487
 
 
488
    typestring = 'unversioned parent'
 
489
 
 
490
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
491
             ' children.  %(action)s.'
 
492
 
 
493
 
 
494
class MissingParent(HandledConflict):
 
495
    """An attempt to add files to a directory that is not present.
 
496
    Typically, the result of a merge where THIS deleted the directory and
 
497
    the OTHER added a file to it.
 
498
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
499
    """
 
500
 
 
501
    typestring = 'missing parent'
 
502
 
 
503
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
504
 
 
505
 
 
506
class DeletingParent(HandledConflict):
 
507
    """An attempt to add files to a directory that is not present.
 
508
    Typically, the result of a merge where one OTHER deleted the directory and
 
509
    the THIS added a file to it.
 
510
    """
 
511
 
 
512
    typestring = 'deleting parent'
 
513
 
 
514
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
515
             "%(action)s."
 
516
 
 
517
 
 
518
class NonDirectoryParent(HandledConflict):
 
519
    """An attempt to add files to a directory that is not a director or
 
520
    an attempt to change the kind of a directory with files.
 
521
    """
 
522
 
 
523
    typestring = 'non-directory parent'
 
524
 
 
525
    format = "Conflict: %(path)s is not a directory, but has files in it."\
 
526
             "  %(action)s."
 
527
 
 
528
ctype = {}
 
529
 
 
530
 
 
531
def register_types(*conflict_types):
 
532
    """Register a Conflict subclass for serialization purposes"""
 
533
    global ctype
 
534
    for conflict_type in conflict_types:
 
535
        ctype[conflict_type.typestring] = conflict_type
 
536
 
 
537
 
 
538
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
539
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
540
               DeletingParent, NonDirectoryParent)