~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Martin Pool
  • Date: 2005-09-13 05:22:41 UTC
  • Revision ID: mbp@sourcefrog.net-20050913052241-52dbd8e8ced620f6
- better BZR_DEBUG trace output

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