~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-19 08:19:38 UTC
  • Revision ID: mbp@sourcefrog.net-20050319081938-596d89f99a644569
use "/usr/bin/env python" for shebang"

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