~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugins/conflicts.py

  • Committer: abentley
  • Date: 2005-10-14 03:50:50 UTC
  • mto: (1185.25.1)
  • mto: This revision was merged to the branch mainline in revision 1460.
  • Revision ID: abentley@lappy-20051014035050-d779472ccb599a51
semi-broke merge

Show diffs side-by-side

added added

removed removed

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