~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Alexander Belchenko
  • Date: 2007-08-14 06:27:51 UTC
  • mto: (2733.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 2734.
  • Revision ID: bialix@ukr.net-20070814062751-tyyn1s5jraunqni9
teach windows python installer to find docs in all subdirectories

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
    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):
 
44
    """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.
 
58
    """
 
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):
 
77
    """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
    """
 
90
    aliases = ['resolved']
 
91
    takes_args = ['file*']
 
92
    takes_options = [
 
93
            Option('all', help='Resolve all conflicts in this tree.'),
 
94
            ]
 
95
    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
 
229
            for suffix in CONFLICT_SUFFIXES:
 
230
                try:
 
231
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
 
232
                except OSError, e:
 
233
                    if e.errno != errno.ENOENT:
 
234
                        raise
 
235
 
 
236
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
237
                         recurse=False):
 
238
        """Select the conflicts associated with paths in a tree.
 
239
        
 
240
        File-ids are also used for this.
 
241
        :return: a pair of ConflictLists: (not_selected, selected)
 
242
        """
 
243
        path_set = set(paths)
 
244
        ids = {}
 
245
        selected_paths = set()
 
246
        new_conflicts = ConflictList()
 
247
        selected_conflicts = ConflictList()
 
248
        for path in paths:
 
249
            file_id = tree.path2id(path)
 
250
            if file_id is not None:
 
251
                ids[file_id] = path
 
252
 
 
253
        for conflict in self:
 
254
            selected = False
 
255
            for key in ('path', 'conflict_path'):
 
256
                cpath = getattr(conflict, key, None)
 
257
                if cpath is None:
 
258
                    continue
 
259
                if cpath in path_set:
 
260
                    selected = True
 
261
                    selected_paths.add(cpath)
 
262
                if recurse:
 
263
                    if osutils.is_inside_any(path_set, cpath):
 
264
                        selected = True
 
265
                        selected_paths.add(cpath)
 
266
 
 
267
            for key in ('file_id', 'conflict_file_id'):
 
268
                cfile_id = getattr(conflict, key, None)
 
269
                if cfile_id is None:
 
270
                    continue
 
271
                try:
 
272
                    cpath = ids[cfile_id]
 
273
                except KeyError:
 
274
                    continue
 
275
                selected = True
 
276
                selected_paths.add(cpath)
 
277
            if selected:
 
278
                selected_conflicts.append(conflict)
 
279
            else:
 
280
                new_conflicts.append(conflict)
 
281
        if ignore_misses is not True:
 
282
            for path in [p for p in paths if p not in selected_paths]:
 
283
                if not os.path.exists(tree.abspath(path)):
 
284
                    print "%s does not exist" % path
 
285
                else:
 
286
                    print "%s is not conflicted" % path
 
287
        return new_conflicts, selected_conflicts
 
288
 
 
289
 
 
290
class Conflict(object):
 
291
    """Base class for all types of conflict"""
 
292
 
 
293
    has_files = False
 
294
 
 
295
    def __init__(self, path, file_id=None):
 
296
        self.path = path
 
297
        # warn turned off, because the factory blindly transfers the Stanza
 
298
        # values to __init__ and Stanza is purely a Unicode api.
 
299
        self.file_id = osutils.safe_file_id(file_id, warn=False)
 
300
 
 
301
    def as_stanza(self):
 
302
        s = rio.Stanza(type=self.typestring, path=self.path)
 
303
        if self.file_id is not None:
 
304
            # Stanza requires Unicode apis
 
305
            s.add('file_id', self.file_id.decode('utf8'))
 
306
        return s
 
307
 
 
308
    def _cmp_list(self):
 
309
        return [type(self), self.path, self.file_id]
 
310
 
 
311
    def __cmp__(self, other):
 
312
        if getattr(other, "_cmp_list", None) is None:
 
313
            return -1
 
314
        return cmp(self._cmp_list(), other._cmp_list())
 
315
 
 
316
    def __hash__(self):
 
317
        return hash((type(self), self.path, self.file_id))
 
318
 
 
319
    def __eq__(self, other):
 
320
        return self.__cmp__(other) == 0
 
321
 
 
322
    def __ne__(self, other):
 
323
        return not self.__eq__(other)
 
324
 
 
325
    def __str__(self):
 
326
        return self.format % self.__dict__
 
327
 
 
328
    def __repr__(self):
 
329
        rdict = dict(self.__dict__)
 
330
        rdict['class'] = self.__class__.__name__
 
331
        return self.rformat % rdict
 
332
 
 
333
    @staticmethod
 
334
    def factory(type, **kwargs):
 
335
        global ctype
 
336
        return ctype[type](**kwargs)
 
337
 
 
338
    @staticmethod
 
339
    def sort_key(conflict):
 
340
        if conflict.path is not None:
 
341
            return conflict.path, conflict.typestring
 
342
        elif getattr(conflict, "conflict_path", None) is not None:
 
343
            return conflict.conflict_path, conflict.typestring
 
344
        else:
 
345
            return None, conflict.typestring
 
346
 
 
347
 
 
348
class PathConflict(Conflict):
 
349
    """A conflict was encountered merging file paths"""
 
350
 
 
351
    typestring = 'path conflict'
 
352
 
 
353
    format = 'Path conflict: %(path)s / %(conflict_path)s'
 
354
 
 
355
    rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
 
356
    def __init__(self, path, conflict_path=None, file_id=None):
 
357
        Conflict.__init__(self, path, file_id)
 
358
        self.conflict_path = conflict_path
 
359
 
 
360
    def as_stanza(self):
 
361
        s = Conflict.as_stanza(self)
 
362
        if self.conflict_path is not None:
 
363
            s.add('conflict_path', self.conflict_path)
 
364
        return s
 
365
 
 
366
 
 
367
class ContentsConflict(PathConflict):
 
368
    """The files are of different types, or not present"""
 
369
 
 
370
    has_files = True
 
371
 
 
372
    typestring = 'contents conflict'
 
373
 
 
374
    format = 'Contents conflict in %(path)s'
 
375
 
 
376
 
 
377
class TextConflict(PathConflict):
 
378
    """The merge algorithm could not resolve all differences encountered."""
 
379
 
 
380
    has_files = True
 
381
 
 
382
    typestring = 'text conflict'
 
383
 
 
384
    format = 'Text conflict in %(path)s'
 
385
 
 
386
 
 
387
class HandledConflict(Conflict):
 
388
    """A path problem that has been provisionally resolved.
 
389
    This is intended to be a base class.
 
390
    """
 
391
 
 
392
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
 
393
    
 
394
    def __init__(self, action, path, file_id=None):
 
395
        Conflict.__init__(self, path, file_id)
 
396
        self.action = action
 
397
 
 
398
    def _cmp_list(self):
 
399
        return Conflict._cmp_list(self) + [self.action]
 
400
 
 
401
    def as_stanza(self):
 
402
        s = Conflict.as_stanza(self)
 
403
        s.add('action', self.action)
 
404
        return s
 
405
 
 
406
 
 
407
class HandledPathConflict(HandledConflict):
 
408
    """A provisionally-resolved path problem involving two paths.
 
409
    This is intended to be a base class.
 
410
    """
 
411
 
 
412
    rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
 
413
        " %(file_id)r, %(conflict_file_id)r)"
 
414
 
 
415
    def __init__(self, action, path, conflict_path, file_id=None,
 
416
                 conflict_file_id=None):
 
417
        HandledConflict.__init__(self, action, path, file_id)
 
418
        self.conflict_path = conflict_path 
 
419
        # warn turned off, because the factory blindly transfers the Stanza
 
420
        # values to __init__.
 
421
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
 
422
                                                     warn=False)
 
423
        
 
424
    def _cmp_list(self):
 
425
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
426
                                                  self.conflict_file_id]
 
427
 
 
428
    def as_stanza(self):
 
429
        s = HandledConflict.as_stanza(self)
 
430
        s.add('conflict_path', self.conflict_path)
 
431
        if self.conflict_file_id is not None:
 
432
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
433
            
 
434
        return s
 
435
 
 
436
 
 
437
class DuplicateID(HandledPathConflict):
 
438
    """Two files want the same file_id."""
 
439
 
 
440
    typestring = 'duplicate id'
 
441
 
 
442
    format = 'Conflict adding id to %(conflict_path)s.  %(action)s %(path)s.'
 
443
 
 
444
 
 
445
class DuplicateEntry(HandledPathConflict):
 
446
    """Two directory entries want to have the same name."""
 
447
 
 
448
    typestring = 'duplicate'
 
449
 
 
450
    format = 'Conflict adding file %(conflict_path)s.  %(action)s %(path)s.'
 
451
 
 
452
 
 
453
class ParentLoop(HandledPathConflict):
 
454
    """An attempt to create an infinitely-looping directory structure.
 
455
    This is rare, but can be produced like so:
 
456
 
 
457
    tree A:
 
458
      mv foo/bar
 
459
    tree B:
 
460
      mv bar/foo
 
461
    merge A and B
 
462
    """
 
463
 
 
464
    typestring = 'parent loop'
 
465
 
 
466
    format = 'Conflict moving %(conflict_path)s into %(path)s.  %(action)s.'
 
467
 
 
468
 
 
469
class UnversionedParent(HandledConflict):
 
470
    """An attempt to version an file whose parent directory is not versioned.
 
471
    Typically, the result of a merge where one tree unversioned the directory
 
472
    and the other added a versioned file to it.
 
473
    """
 
474
 
 
475
    typestring = 'unversioned parent'
 
476
 
 
477
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
478
             ' children.  %(action)s.'
 
479
 
 
480
 
 
481
class MissingParent(HandledConflict):
 
482
    """An attempt to add files to a directory that is not present.
 
483
    Typically, the result of a merge where THIS deleted the directory and
 
484
    the OTHER added a file to it.
 
485
    See also: DeletingParent (same situation, reversed THIS and OTHER)
 
486
    """
 
487
 
 
488
    typestring = 'missing parent'
 
489
 
 
490
    format = 'Conflict adding files to %(path)s.  %(action)s.'
 
491
 
 
492
 
 
493
class DeletingParent(HandledConflict):
 
494
    """An attempt to add files to a directory that is not present.
 
495
    Typically, the result of a merge where one OTHER deleted the directory and
 
496
    the THIS added a file to it.
 
497
    """
 
498
 
 
499
    typestring = 'deleting parent'
 
500
 
 
501
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
502
             "%(action)s."
 
503
 
 
504
 
 
505
ctype = {}
 
506
 
 
507
 
 
508
def register_types(*conflict_types):
 
509
    """Register a Conflict subclass for serialization purposes"""
 
510
    global ctype
 
511
    for conflict_type in conflict_types:
 
512
        ctype[conflict_type.typestring] = conflict_type
 
513
 
 
514
 
 
515
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
 
516
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
517
               DeletingParent,)