~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-21 22:29:49 UTC
  • Revision ID: mbp@sourcefrog.net-20050321222949-232c2093a6eadd80
fixup doctest for new module structure

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Aaron Bentley
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
 
import errno
24
 
 
25
 
import bzrlib
26
 
from bzrlib.commands import register_command
27
 
from bzrlib.errors import BzrCommandError, NotConflicted, UnsupportedOperation
28
 
from bzrlib.option import Option
29
 
from bzrlib.osutils import rename, delete_any
30
 
from bzrlib.rio import Stanza
31
 
 
32
 
 
33
 
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
34
 
 
35
 
 
36
 
class cmd_conflicts(bzrlib.commands.Command):
37
 
    """List files with conflicts.
38
 
 
39
 
    Merge will do its best to combine the changes in two branches, but there
40
 
    are some kinds of problems only a human can fix.  When it encounters those,
41
 
    it will mark a conflict.  A conflict means that you need to fix something,
42
 
    before you should commit.
43
 
 
44
 
    Use bzr resolve when you have fixed a problem.
45
 
 
46
 
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
47
 
    files.)
48
 
 
49
 
    See also bzr resolve.
50
 
    """
51
 
    def run(self):
52
 
        from bzrlib.workingtree import WorkingTree
53
 
        wt = WorkingTree.open_containing(u'.')[0]
54
 
        for conflict in wt.conflicts():
55
 
            print conflict
56
 
 
57
 
 
58
 
class cmd_resolve(bzrlib.commands.Command):
59
 
    """Mark a conflict as resolved.
60
 
 
61
 
    Merge will do its best to combine the changes in two branches, but there
62
 
    are some kinds of problems only a human can fix.  When it encounters those,
63
 
    it will mark a conflict.  A conflict means that you need to fix something,
64
 
    before you should commit.
65
 
 
66
 
    Once you have fixed a problem, use "bzr resolve FILE.." to mark
67
 
    individual files as fixed, or "bzr resolve --all" to mark all conflicts as
68
 
    resolved.
69
 
 
70
 
    See also bzr conflicts.
71
 
    """
72
 
    aliases = ['resolved']
73
 
    takes_args = ['file*']
74
 
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
75
 
    def run(self, file_list=None, all=False):
76
 
        from bzrlib.workingtree import WorkingTree
77
 
        if all:
78
 
            if file_list:
79
 
                raise BzrCommandError("If --all is specified, no FILE may be provided")
80
 
            tree = WorkingTree.open_containing('.')[0]
81
 
            resolve(tree)
82
 
        else:
83
 
            if file_list is None:
84
 
                raise BzrCommandError("command 'resolve' needs one or more FILE, or --all")
85
 
            tree = WorkingTree.open_containing(file_list[0])[0]
86
 
            to_resolve = [tree.relpath(p) for p in file_list]
87
 
            resolve(tree, to_resolve)
88
 
 
89
 
 
90
 
def resolve(tree, paths=None, ignore_misses=False):
91
 
    tree.lock_tree_write()
92
 
    try:
93
 
        tree_conflicts = tree.conflicts()
94
 
        if paths is None:
95
 
            new_conflicts = ConflictList()
96
 
            selected_conflicts = tree_conflicts
97
 
        else:
98
 
            new_conflicts, selected_conflicts = \
99
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses)
100
 
        try:
101
 
            tree.set_conflicts(new_conflicts)
102
 
        except UnsupportedOperation:
103
 
            pass
104
 
        selected_conflicts.remove_files(tree)
105
 
    finally:
106
 
        tree.unlock()
107
 
 
108
 
 
109
 
def restore(filename):
110
 
    """\
111
 
    Restore a conflicted file to the state it was in before merging.
112
 
    Only text restoration supported at present.
113
 
    """
114
 
    conflicted = False
115
 
    try:
116
 
        rename(filename + ".THIS", filename)
117
 
        conflicted = True
118
 
    except OSError, e:
119
 
        if e.errno != errno.ENOENT:
120
 
            raise
121
 
    try:
122
 
        os.unlink(filename + ".BASE")
123
 
        conflicted = True
124
 
    except OSError, e:
125
 
        if e.errno != errno.ENOENT:
126
 
            raise
127
 
    try:
128
 
        os.unlink(filename + ".OTHER")
129
 
        conflicted = True
130
 
    except OSError, e:
131
 
        if e.errno != errno.ENOENT:
132
 
            raise
133
 
    if not conflicted:
134
 
        raise NotConflicted(filename)
135
 
 
136
 
 
137
 
class ConflictList(object):
138
 
    """List of conflicts.
139
 
 
140
 
    Typically obtained from WorkingTree.conflicts()
141
 
 
142
 
    Can be instantiated from stanzas or from Conflict subclasses.
143
 
    """
144
 
 
145
 
    def __init__(self, conflicts=None):
146
 
        object.__init__(self)
147
 
        if conflicts is None:
148
 
            self.__list = []
149
 
        else:
150
 
            self.__list = conflicts
151
 
 
152
 
    def is_empty(self):
153
 
        return len(self.__list) == 0
154
 
 
155
 
    def __len__(self):
156
 
        return len(self.__list)
157
 
 
158
 
    def __iter__(self):
159
 
        return iter(self.__list)
160
 
 
161
 
    def __getitem__(self, key):
162
 
        return self.__list[key]
163
 
 
164
 
    def append(self, conflict):
165
 
        return self.__list.append(conflict)
166
 
 
167
 
    def __eq__(self, other_list):
168
 
        return list(self) == list(other_list)
169
 
 
170
 
    def __ne__(self, other_list):
171
 
        return not (self == other_list)
172
 
 
173
 
    def __repr__(self):
174
 
        return "ConflictList(%r)" % self.__list
175
 
 
176
 
    @staticmethod
177
 
    def from_stanzas(stanzas):
178
 
        """Produce a new ConflictList from an iterable of stanzas"""
179
 
        conflicts = ConflictList()
180
 
        for stanza in stanzas:
181
 
            conflicts.append(Conflict.factory(**stanza.as_dict()))
182
 
        return conflicts
183
 
 
184
 
    def to_stanzas(self):
185
 
        """Generator of stanzas"""
186
 
        for conflict in self:
187
 
            yield conflict.as_stanza()
188
 
            
189
 
    def to_strings(self):
190
 
        """Generate strings for the provided conflicts"""
191
 
        for conflict in self:
192
 
            yield str(conflict)
193
 
 
194
 
    def remove_files(self, tree):
195
 
        """Remove the THIS, BASE and OTHER files for listed conflicts"""
196
 
        for conflict in self:
197
 
            if not conflict.has_files:
198
 
                continue
199
 
            for suffix in CONFLICT_SUFFIXES:
200
 
                try:
201
 
                    delete_any(tree.abspath(conflict.path+suffix))
202
 
                except OSError, e:
203
 
                    if e.errno != errno.ENOENT:
204
 
                        raise
205
 
 
206
 
    def select_conflicts(self, tree, paths, ignore_misses=False):
207
 
        """Select the conflicts associated with paths in a tree.
208
 
        
209
 
        File-ids are also used for this.
210
 
        :return: a pair of ConflictLists: (not_selected, selected)
211
 
        """
212
 
        path_set = set(paths)
213
 
        ids = {}
214
 
        selected_paths = set()
215
 
        new_conflicts = ConflictList()
216
 
        selected_conflicts = ConflictList()
217
 
        for path in paths:
218
 
            file_id = tree.path2id(path)
219
 
            if file_id is not None:
220
 
                ids[file_id] = path
221
 
 
222
 
        for conflict in self:
223
 
            selected = False
224
 
            for key in ('path', 'conflict_path'):
225
 
                cpath = getattr(conflict, key, None)
226
 
                if cpath is None:
227
 
                    continue
228
 
                if cpath in path_set:
229
 
                    selected = True
230
 
                    selected_paths.add(cpath)
231
 
            for key in ('file_id', 'conflict_file_id'):
232
 
                cfile_id = getattr(conflict, key, None)
233
 
                if cfile_id is None:
234
 
                    continue
235
 
                try:
236
 
                    cpath = ids[cfile_id]
237
 
                except KeyError:
238
 
                    continue
239
 
                selected = True
240
 
                selected_paths.add(cpath)
241
 
            if selected:
242
 
                selected_conflicts.append(conflict)
243
 
            else:
244
 
                new_conflicts.append(conflict)
245
 
        if ignore_misses is not True:
246
 
            for path in [p for p in paths if p not in selected_paths]:
247
 
                if not os.path.exists(tree.abspath(path)):
248
 
                    print "%s does not exist" % path
249
 
                else:
250
 
                    print "%s is not conflicted" % path
251
 
        return new_conflicts, selected_conflicts
252
 
 
253
 
 
254
 
class Conflict(object):
255
 
    """Base class for all types of conflict"""
256
 
 
257
 
    has_files = False
258
 
 
259
 
    def __init__(self, path, file_id=None):
260
 
        self.path = path
261
 
        self.file_id = file_id
262
 
 
263
 
    def as_stanza(self):
264
 
        s = Stanza(type=self.typestring, path=self.path)
265
 
        if self.file_id is not None:
266
 
            s.add('file_id', self.file_id)
267
 
        return s
268
 
 
269
 
    def _cmp_list(self):
270
 
        return [type(self), self.path, self.file_id]
271
 
 
272
 
    def __cmp__(self, other):
273
 
        if getattr(other, "_cmp_list", None) is None:
274
 
            return -1
275
 
        return cmp(self._cmp_list(), other._cmp_list())
276
 
 
277
 
    def __hash__(self):
278
 
        return hash((type(self), self.path, self.file_id))
279
 
 
280
 
    def __eq__(self, other):
281
 
        return self.__cmp__(other) == 0
282
 
 
283
 
    def __ne__(self, other):
284
 
        return not self.__eq__(other)
285
 
 
286
 
    def __str__(self):
287
 
        return self.format % self.__dict__
288
 
 
289
 
    def __repr__(self):
290
 
        rdict = dict(self.__dict__)
291
 
        rdict['class'] = self.__class__.__name__
292
 
        return self.rformat % rdict
293
 
 
294
 
    @staticmethod
295
 
    def factory(type, **kwargs):
296
 
        global ctype
297
 
        return ctype[type](**kwargs)
298
 
 
299
 
    @staticmethod
300
 
    def sort_key(conflict):
301
 
        if conflict.path is not None:
302
 
            return conflict.path, conflict.typestring
303
 
        elif getattr(conflict, "conflict_path", None) is not None:
304
 
            return conflict.conflict_path, conflict.typestring
305
 
        else:
306
 
            return None, conflict.typestring
307
 
 
308
 
 
309
 
class PathConflict(Conflict):
310
 
    """A conflict was encountered merging file paths"""
311
 
 
312
 
    typestring = 'path conflict'
313
 
 
314
 
    format = 'Path conflict: %(path)s / %(conflict_path)s'
315
 
 
316
 
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
317
 
    def __init__(self, path, conflict_path=None, file_id=None):
318
 
        Conflict.__init__(self, path, file_id)
319
 
        self.conflict_path = conflict_path
320
 
 
321
 
    def as_stanza(self):
322
 
        s = Conflict.as_stanza(self)
323
 
        if self.conflict_path is not None:
324
 
            s.add('conflict_path', self.conflict_path)
325
 
        return s
326
 
 
327
 
 
328
 
class ContentsConflict(PathConflict):
329
 
    """The files are of different types, or not present"""
330
 
 
331
 
    has_files = True
332
 
 
333
 
    typestring = 'contents conflict'
334
 
 
335
 
    format = 'Contents conflict in %(path)s'
336
 
 
337
 
 
338
 
class TextConflict(PathConflict):
339
 
    """The merge algorithm could not resolve all differences encountered."""
340
 
 
341
 
    has_files = True
342
 
 
343
 
    typestring = 'text conflict'
344
 
 
345
 
    format = 'Text conflict in %(path)s'
346
 
 
347
 
 
348
 
class HandledConflict(Conflict):
349
 
    """A path problem that has been provisionally resolved.
350
 
    This is intended to be a base class.
351
 
    """
352
 
 
353
 
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
354
 
    
355
 
    def __init__(self, action, path, file_id=None):
356
 
        Conflict.__init__(self, path, file_id)
357
 
        self.action = action
358
 
 
359
 
    def _cmp_list(self):
360
 
        return Conflict._cmp_list(self) + [self.action]
361
 
 
362
 
    def as_stanza(self):
363
 
        s = Conflict.as_stanza(self)
364
 
        s.add('action', self.action)
365
 
        return s
366
 
 
367
 
 
368
 
class HandledPathConflict(HandledConflict):
369
 
    """A provisionally-resolved path problem involving two paths.
370
 
    This is intended to be a base class.
371
 
    """
372
 
 
373
 
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
374
 
        " %(file_id)r, %(conflict_file_id)r)"
375
 
 
376
 
    def __init__(self, action, path, conflict_path, file_id=None,
377
 
                 conflict_file_id=None):
378
 
        HandledConflict.__init__(self, action, path, file_id)
379
 
        self.conflict_path = conflict_path 
380
 
        self.conflict_file_id = conflict_file_id
381
 
        
382
 
    def _cmp_list(self):
383
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
384
 
                                                  self.conflict_file_id]
385
 
 
386
 
    def as_stanza(self):
387
 
        s = HandledConflict.as_stanza(self)
388
 
        s.add('conflict_path', self.conflict_path)
389
 
        if self.conflict_file_id is not None:
390
 
            s.add('conflict_file_id', self.conflict_file_id)
391
 
            
392
 
        return s
393
 
 
394
 
 
395
 
class DuplicateID(HandledPathConflict):
396
 
    """Two files want the same file_id."""
397
 
 
398
 
    typestring = 'duplicate id'
399
 
 
400
 
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
401
 
 
402
 
 
403
 
class DuplicateEntry(HandledPathConflict):
404
 
    """Two directory entries want to have the same name."""
405
 
 
406
 
    typestring = 'duplicate'
407
 
 
408
 
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
409
 
 
410
 
 
411
 
class ParentLoop(HandledPathConflict):
412
 
    """An attempt to create an infinitely-looping directory structure.
413
 
    This is rare, but can be produced like so:
414
 
 
415
 
    tree A:
416
 
      mv foo/bar
417
 
    tree B:
418
 
      mv bar/foo
419
 
    merge A and B
420
 
    """
421
 
 
422
 
    typestring = 'parent loop'
423
 
 
424
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
425
 
 
426
 
 
427
 
class UnversionedParent(HandledConflict):
428
 
    """An attempt to version an file whose parent directory is not versioned.
429
 
    Typically, the result of a merge where one tree unversioned the directory
430
 
    and the other added a versioned file to it.
431
 
    """
432
 
 
433
 
    typestring = 'unversioned parent'
434
 
 
435
 
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
436
 
             ' children.  %(action)s.'
437
 
 
438
 
 
439
 
class MissingParent(HandledConflict):
440
 
    """An attempt to add files to a directory that is not present.
441
 
    Typically, the result of a merge where THIS deleted the directory and
442
 
    the OTHER added a file to it.
443
 
    See also: DeletingParent (same situation, reversed THIS and OTHER)
444
 
    """
445
 
 
446
 
    typestring = 'missing parent'
447
 
 
448
 
    format = 'Conflict adding files to %(path)s.  %(action)s.'
449
 
 
450
 
 
451
 
class DeletingParent(HandledConflict):
452
 
    """An attempt to add files to a directory that is not present.
453
 
    Typically, the result of a merge where one OTHER deleted the directory and
454
 
    the THIS added a file to it.
455
 
    """
456
 
 
457
 
    typestring = 'deleting parent'
458
 
 
459
 
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
460
 
             "%(action)s."
461
 
 
462
 
 
463
 
ctype = {}
464
 
 
465
 
 
466
 
def register_types(*conflict_types):
467
 
    """Register a Conflict subclass for serialization purposes"""
468
 
    global ctype
469
 
    for conflict_type in conflict_types:
470
 
        ctype[conflict_type.typestring] = conflict_type
471
 
 
472
 
 
473
 
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
474
 
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
475
 
               DeletingParent,)