~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Martin Pool
  • Date: 2005-05-03 08:00:27 UTC
  • Revision ID: mbp@sourcefrog.net-20050503080027-908edb5b39982198
doc

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_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
 
        """
211
 
        path_set = set(paths)
212
 
        ids = {}
213
 
        selected_paths = set()
214
 
        new_conflicts = ConflictList()
215
 
        selected_conflicts = ConflictList()
216
 
        for path in paths:
217
 
            file_id = tree.path2id(path)
218
 
            if file_id is not None:
219
 
                ids[file_id] = path
220
 
 
221
 
        for conflict in self:
222
 
            selected = False
223
 
            for key in ('path', 'conflict_path'):
224
 
                cpath = getattr(conflict, key, None)
225
 
                if cpath is None:
226
 
                    continue
227
 
                if cpath in path_set:
228
 
                    selected = True
229
 
                    selected_paths.add(cpath)
230
 
            for key in ('file_id', 'conflict_file_id'):
231
 
                cfile_id = getattr(conflict, key, None)
232
 
                if cfile_id is None:
233
 
                    continue
234
 
                try:
235
 
                    cpath = ids[cfile_id]
236
 
                except KeyError:
237
 
                    continue
238
 
                selected = True
239
 
                selected_paths.add(cpath)
240
 
            if selected:
241
 
                selected_conflicts.append(conflict)
242
 
            else:
243
 
                new_conflicts.append(conflict)
244
 
        if ignore_misses is not True:
245
 
            for path in [p for p in paths if p not in selected_paths]:
246
 
                if not os.path.exists(tree.abspath(path)):
247
 
                    print "%s does not exist" % path
248
 
                else:
249
 
                    print "%s is not conflicted" % path
250
 
        return new_conflicts, selected_conflicts
251
 
 
252
 
 
253
 
class Conflict(object):
254
 
    """Base class for all types of conflict"""
255
 
 
256
 
    has_files = False
257
 
 
258
 
    def __init__(self, path, file_id=None):
259
 
        self.path = path
260
 
        self.file_id = file_id
261
 
 
262
 
    def as_stanza(self):
263
 
        s = Stanza(type=self.typestring, path=self.path)
264
 
        if self.file_id is not None:
265
 
            s.add('file_id', self.file_id)
266
 
        return s
267
 
 
268
 
    def _cmp_list(self):
269
 
        return [type(self), self.path, self.file_id]
270
 
 
271
 
    def __cmp__(self, other):
272
 
        if getattr(other, "_cmp_list", None) is None:
273
 
            return -1
274
 
        return cmp(self._cmp_list(), other._cmp_list())
275
 
 
276
 
    def __eq__(self, other):
277
 
        return self.__cmp__(other) == 0
278
 
 
279
 
    def __ne__(self, other):
280
 
        return not self.__eq__(other)
281
 
 
282
 
    def __str__(self):
283
 
        return self.format % self.__dict__
284
 
 
285
 
    def __repr__(self):
286
 
        rdict = dict(self.__dict__)
287
 
        rdict['class'] = self.__class__.__name__
288
 
        return self.rformat % rdict
289
 
 
290
 
    @staticmethod
291
 
    def factory(type, **kwargs):
292
 
        global ctype
293
 
        return ctype[type](**kwargs)
294
 
 
295
 
    @staticmethod
296
 
    def sort_key(conflict):
297
 
        if conflict.path is not None:
298
 
            return conflict.path, conflict.typestring
299
 
        elif getattr(conflict, "conflict_path", None) is not None:
300
 
            return conflict.conflict_path, conflict.typestring
301
 
        else:
302
 
            return None, conflict.typestring
303
 
 
304
 
 
305
 
class PathConflict(Conflict):
306
 
    """A conflict was encountered merging file paths"""
307
 
 
308
 
    typestring = 'path conflict'
309
 
 
310
 
    format = 'Path conflict: %(path)s / %(conflict_path)s'
311
 
 
312
 
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
313
 
    def __init__(self, path, conflict_path=None, file_id=None):
314
 
        Conflict.__init__(self, path, file_id)
315
 
        self.conflict_path = conflict_path
316
 
 
317
 
    def as_stanza(self):
318
 
        s = Conflict.as_stanza(self)
319
 
        if self.conflict_path is not None:
320
 
            s.add('conflict_path', self.conflict_path)
321
 
        return s
322
 
 
323
 
 
324
 
class ContentsConflict(PathConflict):
325
 
    """The files are of different types, or not present"""
326
 
 
327
 
    has_files = True
328
 
 
329
 
    typestring = 'contents conflict'
330
 
 
331
 
    format = 'Contents conflict in %(path)s'
332
 
 
333
 
 
334
 
class TextConflict(PathConflict):
335
 
    """The merge algorithm could not resolve all differences encountered."""
336
 
 
337
 
    has_files = True
338
 
 
339
 
    typestring = 'text conflict'
340
 
 
341
 
    format = 'Text conflict in %(path)s'
342
 
 
343
 
 
344
 
class HandledConflict(Conflict):
345
 
    """A path problem that has been provisionally resolved.
346
 
    This is intended to be a base class.
347
 
    """
348
 
 
349
 
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
350
 
    
351
 
    def __init__(self, action, path, file_id=None):
352
 
        Conflict.__init__(self, path, file_id)
353
 
        self.action = action
354
 
 
355
 
    def _cmp_list(self):
356
 
        return Conflict._cmp_list(self) + [self.action]
357
 
 
358
 
    def as_stanza(self):
359
 
        s = Conflict.as_stanza(self)
360
 
        s.add('action', self.action)
361
 
        return s
362
 
 
363
 
 
364
 
class HandledPathConflict(HandledConflict):
365
 
    """A provisionally-resolved path problem involving two paths.
366
 
    This is intended to be a base class.
367
 
    """
368
 
 
369
 
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
370
 
        " %(file_id)r, %(conflict_file_id)r)"
371
 
 
372
 
    def __init__(self, action, path, conflict_path, file_id=None,
373
 
                 conflict_file_id=None):
374
 
        HandledConflict.__init__(self, action, path, file_id)
375
 
        self.conflict_path = conflict_path 
376
 
        self.conflict_file_id = conflict_file_id
377
 
        
378
 
    def _cmp_list(self):
379
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
380
 
                                                  self.conflict_file_id]
381
 
 
382
 
    def as_stanza(self):
383
 
        s = HandledConflict.as_stanza(self)
384
 
        s.add('conflict_path', self.conflict_path)
385
 
        if self.conflict_file_id is not None:
386
 
            s.add('conflict_file_id', self.conflict_file_id)
387
 
            
388
 
        return s
389
 
 
390
 
 
391
 
class DuplicateID(HandledPathConflict):
392
 
    """Two files want the same file_id."""
393
 
 
394
 
    typestring = 'duplicate id'
395
 
 
396
 
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
397
 
 
398
 
 
399
 
class DuplicateEntry(HandledPathConflict):
400
 
    """Two directory entries want to have the same name."""
401
 
 
402
 
    typestring = 'duplicate'
403
 
 
404
 
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
405
 
 
406
 
 
407
 
class ParentLoop(HandledPathConflict):
408
 
    """An attempt to create an infinitely-looping directory structure.
409
 
    This is rare, but can be produced like so:
410
 
 
411
 
    tree A:
412
 
      mv foo/bar
413
 
    tree B:
414
 
      mv bar/foo
415
 
    merge A and B
416
 
    """
417
 
 
418
 
    typestring = 'parent loop'
419
 
 
420
 
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
421
 
 
422
 
 
423
 
class UnversionedParent(HandledConflict):
424
 
    """An attempt to version an file whose parent directory is not versioned.
425
 
    Typically, the result of a merge where one tree unversioned the directory
426
 
    and the other added a versioned file to it.
427
 
    """
428
 
 
429
 
    typestring = 'unversioned parent'
430
 
 
431
 
    format = 'Conflict adding versioned files to %(path)s.  %(action)s.'
432
 
 
433
 
 
434
 
class MissingParent(HandledConflict):
435
 
    """An attempt to add files to a directory that is not present.
436
 
    Typically, the result of a merge where one tree deleted the directory and
437
 
    the other added a file to it.
438
 
    """
439
 
 
440
 
    typestring = 'missing parent'
441
 
 
442
 
    format = 'Conflict adding files to %(path)s.  %(action)s.'
443
 
 
444
 
 
445
 
 
446
 
ctype = {}
447
 
 
448
 
 
449
 
def register_types(*conflict_types):
450
 
    """Register a Conflict subclass for serialization purposes"""
451
 
    global ctype
452
 
    for conflict_type in conflict_types:
453
 
        ctype[conflict_type.typestring] = conflict_type
454
 
 
455
 
 
456
 
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
457
 
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)