~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Robey Pointer
  • Date: 2006-09-08 18:46:29 UTC
  • mto: This revision was merged to the branch mainline in revision 1996.
  • Revision ID: robey@lag.net-20060908184629-e3fc4c61ca21508c
pychecker is on crack; go back to using 'is None'.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2005 by Aaron Bentley
2
 
 
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
19
# TODO: 'bzr resolve' should accept a directory name and work from that 
20
20
# point down
21
21
 
22
 
# TODO: bzr revert should resolve; even when reverting the whole tree
23
 
# or particular directories
24
 
 
25
22
import os
26
23
import errno
27
24
 
28
 
import bzrlib.status
29
 
from bzrlib.branch import Branch
30
 
from bzrlib.errors import BzrCommandError, NotConflicted
 
25
import bzrlib
31
26
from bzrlib.commands import register_command
32
 
from bzrlib.workingtree import CONFLICT_SUFFIXES
 
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
 
33
35
 
34
36
class cmd_conflicts(bzrlib.commands.Command):
35
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
 
36
46
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
37
47
    files.)
 
48
 
 
49
    See also bzr resolve.
38
50
    """
39
51
    def run(self):
40
 
        for path in Branch.open_containing(u'.')[0].working_tree().iter_conflicts():
41
 
            print path
 
52
        from bzrlib.workingtree import WorkingTree
 
53
        wt = WorkingTree.open_containing(u'.')[0]
 
54
        for conflict in wt.conflicts():
 
55
            print conflict
 
56
 
42
57
 
43
58
class cmd_resolve(bzrlib.commands.Command):
44
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.
45
71
    """
46
72
    aliases = ['resolved']
47
73
    takes_args = ['file*']
48
 
    takes_options = ['all']
 
74
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
49
75
    def run(self, file_list=None, all=False):
50
 
        if file_list is None:
51
 
            if not all:
52
 
                raise BzrCommandError(
53
 
                    "command 'resolve' needs one or more FILE, or --all")
54
 
            tree = Branch.open_containing(u'.')[0].working_tree()
55
 
            file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
56
 
        else:
57
 
            if all:
58
 
                raise BzrCommandError(
59
 
                    "If --all is specified, no FILE may be provided")
60
 
        for filename in file_list:
61
 
            failures = 0
62
 
            for suffix in CONFLICT_SUFFIXES:
63
 
                try:
64
 
                    os.unlink(filename+suffix)
65
 
                except OSError, e:
66
 
                    if e.errno != errno.ENOENT:
67
 
                        raise
68
 
                    else:
69
 
                        failures += 1
70
 
            if failures == len(CONFLICT_SUFFIXES):
71
 
                if not os.path.exists(filename):
72
 
                    print "%s does not exist" % filename
73
 
                else:
74
 
                    print "%s is not conflicted" % filename
 
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
 
75
108
 
76
109
def restore(filename):
77
110
    """\
80
113
    """
81
114
    conflicted = False
82
115
    try:
83
 
        os.rename(filename + ".THIS", filename)
 
116
        rename(filename + ".THIS", filename)
84
117
        conflicted = True
85
118
    except OSError, e:
86
119
        if e.errno != errno.ENOENT:
99
132
            raise
100
133
    if not conflicted:
101
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 adding versioned files to %(path)s.  %(action)s.'
 
436
 
 
437
 
 
438
class MissingParent(HandledConflict):
 
439
    """An attempt to add files to a directory that is not present.
 
440
    Typically, the result of a merge where one tree deleted the directory and
 
441
    the other added a file to it.
 
442
    """
 
443
 
 
444
    typestring = 'missing parent'
 
445
 
 
446
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
447
 
 
448
 
 
449
 
 
450
ctype = {}
 
451
 
 
452
 
 
453
def register_types(*conflict_types):
 
454
    """Register a Conflict subclass for serialization purposes"""
 
455
    global ctype
 
456
    for conflict_type in conflict_types:
 
457
        ctype[conflict_type.typestring] = conflict_type
 
458
 
 
459
 
 
460
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
461
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)