/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: Alexander Belchenko
  • Date: 2007-03-06 08:05:08 UTC
  • mto: This revision was merged to the branch mainline in revision 2319.
  • Revision ID: bialix@ukr.net-20070306080508-43s3c6gz2nqikt8m
update comment about stderr (trivial)

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
        self.file_id = file_id
 
287
 
 
288
    def as_stanza(self):
 
289
        s = rio.Stanza(type=self.typestring, path=self.path)
 
290
        if self.file_id is not None:
 
291
            s.add('file_id', self.file_id)
 
292
        return s
 
293
 
 
294
    def _cmp_list(self):
 
295
        return [type(self), self.path, self.file_id]
 
296
 
 
297
    def __cmp__(self, other):
 
298
        if getattr(other, "_cmp_list", None) is None:
 
299
            return -1
 
300
        return cmp(self._cmp_list(), other._cmp_list())
 
301
 
 
302
    def __hash__(self):
 
303
        return hash((type(self), self.path, self.file_id))
 
304
 
 
305
    def __eq__(self, other):
 
306
        return self.__cmp__(other) == 0
 
307
 
 
308
    def __ne__(self, other):
 
309
        return not self.__eq__(other)
 
310
 
 
311
    def __str__(self):
 
312
        return self.format % self.__dict__
 
313
 
 
314
    def __repr__(self):
 
315
        rdict = dict(self.__dict__)
 
316
        rdict['class'] = self.__class__.__name__
 
317
        return self.rformat % rdict
 
318
 
 
319
    @staticmethod
 
320
    def factory(type, **kwargs):
 
321
        global ctype
 
322
        return ctype[type](**kwargs)
 
323
 
 
324
    @staticmethod
 
325
    def sort_key(conflict):
 
326
        if conflict.path is not None:
 
327
            return conflict.path, conflict.typestring
 
328
        elif getattr(conflict, "conflict_path", None) is not None:
 
329
            return conflict.conflict_path, conflict.typestring
 
330
        else:
 
331
            return None, conflict.typestring
 
332
 
 
333
 
 
334
class PathConflict(Conflict):
 
335
    """A conflict was encountered merging file paths"""
 
336
 
 
337
    typestring = 'path conflict'
 
338
 
 
339
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
340
 
 
341
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
342
    def __init__(self, path, conflict_path=None, file_id=None):
 
343
        Conflict.__init__(self, path, file_id)
 
344
        self.conflict_path = conflict_path
 
345
 
 
346
    def as_stanza(self):
 
347
        s = Conflict.as_stanza(self)
 
348
        if self.conflict_path is not None:
 
349
            s.add('conflict_path', self.conflict_path)
 
350
        return s
 
351
 
 
352
 
 
353
class ContentsConflict(PathConflict):
 
354
    """The files are of different types, or not present"""
 
355
 
 
356
    has_files = True
 
357
 
 
358
    typestring = 'contents conflict'
 
359
 
 
360
    format = 'Contents conflict in %(path)s'
 
361
 
 
362
 
 
363
class TextConflict(PathConflict):
 
364
    """The merge algorithm could not resolve all differences encountered."""
 
365
 
 
366
    has_files = True
 
367
 
 
368
    typestring = 'text conflict'
 
369
 
 
370
    format = 'Text conflict in %(path)s'
 
371
 
 
372
 
 
373
class HandledConflict(Conflict):
 
374
    """A path problem that has been provisionally resolved.
 
375
    This is intended to be a base class.
 
376
    """
 
377
 
 
378
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
379
    
 
380
    def __init__(self, action, path, file_id=None):
 
381
        Conflict.__init__(self, path, file_id)
 
382
        self.action = action
 
383
 
 
384
    def _cmp_list(self):
 
385
        return Conflict._cmp_list(self) + [self.action]
 
386
 
 
387
    def as_stanza(self):
 
388
        s = Conflict.as_stanza(self)
 
389
        s.add('action', self.action)
 
390
        return s
 
391
 
 
392
 
 
393
class HandledPathConflict(HandledConflict):
 
394
    """A provisionally-resolved path problem involving two paths.
 
395
    This is intended to be a base class.
 
396
    """
 
397
 
 
398
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
399
        " %(file_id)r, %(conflict_file_id)r)"
 
400
 
 
401
    def __init__(self, action, path, conflict_path, file_id=None,
 
402
                 conflict_file_id=None):
 
403
        HandledConflict.__init__(self, action, path, file_id)
 
404
        self.conflict_path = conflict_path 
 
405
        self.conflict_file_id = conflict_file_id
 
406
        
 
407
    def _cmp_list(self):
 
408
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
409
                                                  self.conflict_file_id]
 
410
 
 
411
    def as_stanza(self):
 
412
        s = HandledConflict.as_stanza(self)
 
413
        s.add('conflict_path', self.conflict_path)
 
414
        if self.conflict_file_id is not None:
 
415
            s.add('conflict_file_id', self.conflict_file_id)
 
416
            
 
417
        return s
 
418
 
 
419
 
 
420
class DuplicateID(HandledPathConflict):
 
421
    """Two files want the same file_id."""
 
422
 
 
423
    typestring = 'duplicate id'
 
424
 
 
425
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
426
 
 
427
 
 
428
class DuplicateEntry(HandledPathConflict):
 
429
    """Two directory entries want to have the same name."""
 
430
 
 
431
    typestring = 'duplicate'
 
432
 
 
433
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
434
 
 
435
 
 
436
class ParentLoop(HandledPathConflict):
 
437
    """An attempt to create an infinitely-looping directory structure.
 
438
    This is rare, but can be produced like so:
 
439
 
 
440
    tree A:
 
441
      mv foo/bar
 
442
    tree B:
 
443
      mv bar/foo
 
444
    merge A and B
 
445
    """
 
446
 
 
447
    typestring = 'parent loop'
 
448
 
 
449
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
450
 
 
451
 
 
452
class UnversionedParent(HandledConflict):
 
453
    """An attempt to version an file whose parent directory is not versioned.
 
454
    Typically, the result of a merge where one tree unversioned the directory
 
455
    and the other added a versioned file to it.
 
456
    """
 
457
 
 
458
    typestring = 'unversioned parent'
 
459
 
 
460
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
461
             ' children.  %(action)s.'
 
462
 
 
463
 
 
464
class MissingParent(HandledConflict):
 
465
    """An attempt to add files to a directory that is not present.
 
466
    Typically, the result of a merge where THIS deleted the directory and
 
467
    the OTHER added a file to it.
 
468
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
469
    """
 
470
 
 
471
    typestring = 'missing parent'
 
472
 
 
473
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
474
 
 
475
 
 
476
class DeletingParent(HandledConflict):
 
477
    """An attempt to add files to a directory that is not present.
 
478
    Typically, the result of a merge where one OTHER deleted the directory and
 
479
    the THIS added a file to it.
 
480
    """
 
481
 
 
482
    typestring = 'deleting parent'
 
483
 
 
484
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
485
             "%(action)s."
 
486
 
 
487
 
 
488
ctype = {}
 
489
 
 
490
 
 
491
def register_types(*conflict_types):
 
492
    """Register a Conflict subclass for serialization purposes"""
 
493
    global ctype
 
494
    for conflict_type in conflict_types:
 
495
        ctype[conflict_type.typestring] = conflict_type
 
496
 
 
497
 
 
498
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
499
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
500
               DeletingParent,)