~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: John Arbash Meinel
  • Date: 2007-06-29 16:16:46 UTC
  • mto: This revision was merged to the branch mainline in revision 2614.
  • Revision ID: john@arbash-meinel.com-20070629161646-ufelk4s0m1ig5md8
Don't suppress the TypeError if it doesn't match our requirements.

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,)