/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Aaron Bentley
  • Date: 2007-11-23 20:19:57 UTC
  • mto: This revision was merged to the branch mainline in revision 3021.
  • Revision ID: abentley@panoramicfeedback.com-20071123201957-xntyi59nfugfz2u1
Revert now resolves conflicts recursively (#102739)

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