~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

  • Committer: Martin Pool
  • Date: 2010-01-29 14:09:05 UTC
  • mto: This revision was merged to the branch mainline in revision 4992.
  • Revision ID: mbp@sourcefrog.net-20100129140905-2uiarb6p8di1ywsr
Correction to url

from review: https://code.edge.launchpad.net/~mbp/bzr/doc/+merge/18250

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Aaron Bentley
 
1
# Copyright (C) 2005, 2007 Canonical Ltd
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
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
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
# TODO: Move this into builtins
18
18
 
19
 
# TODO: 'bzr resolve' should accept a directory name and work from that 
 
19
# TODO: 'bzr resolve' should accept a directory name and work from that
20
20
# point down
21
21
 
22
22
import os
 
23
 
 
24
from bzrlib.lazy_import import lazy_import
 
25
lazy_import(globals(), """
23
26
import errno
24
27
 
25
 
import bzrlib
26
 
from bzrlib.commands import register_command
27
 
from bzrlib.errors import BzrCommandError, NotConflicted, UnsupportedOperation
 
28
from bzrlib import (
 
29
    builtins,
 
30
    commands,
 
31
    errors,
 
32
    osutils,
 
33
    rio,
 
34
    trace,
 
35
    )
 
36
""")
28
37
from bzrlib.option import Option
29
 
from bzrlib.osutils import rename, delete_any
30
 
from bzrlib.rio import Stanza
31
38
 
32
39
 
33
40
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
34
41
 
35
42
 
36
 
class cmd_conflicts(bzrlib.commands.Command):
 
43
class cmd_conflicts(commands.Command):
37
44
    """List files with conflicts.
38
45
 
39
46
    Merge will do its best to combine the changes in two branches, but there
41
48
    it will mark a conflict.  A conflict means that you need to fix something,
42
49
    before you should commit.
43
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
 
44
55
    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
56
    """
51
 
    def run(self):
 
57
    takes_options = [
 
58
            Option('text',
 
59
                   help='List paths of files with text conflicts.'),
 
60
        ]
 
61
    _see_also = ['resolve', 'conflict-types']
 
62
 
 
63
    def run(self, text=False):
52
64
        from bzrlib.workingtree import WorkingTree
53
65
        wt = WorkingTree.open_containing(u'.')[0]
54
66
        for conflict in wt.conflicts():
55
 
            print conflict
56
 
 
57
 
 
58
 
class cmd_resolve(bzrlib.commands.Command):
 
67
            if text:
 
68
                if conflict.typestring != 'text conflict':
 
69
                    continue
 
70
                self.outf.write(conflict.path + '\n')
 
71
            else:
 
72
                self.outf.write(str(conflict) + '\n')
 
73
 
 
74
 
 
75
class cmd_resolve(commands.Command):
59
76
    """Mark a conflict as resolved.
60
77
 
61
78
    Merge will do its best to combine the changes in two branches, but there
63
80
    it will mark a conflict.  A conflict means that you need to fix something,
64
81
    before you should commit.
65
82
 
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.
 
83
    Once you have fixed a problem, use "bzr resolve" to automatically mark
 
84
    text conflicts as fixed, "bzr resolve FILE" to mark a specific conflict as
 
85
    resolved, or "bzr resolve --all" to mark all conflicts as resolved.
71
86
    """
72
87
    aliases = ['resolved']
73
88
    takes_args = ['file*']
74
 
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
 
89
    takes_options = [
 
90
            Option('all', help='Resolve all conflicts in this tree.'),
 
91
            ]
 
92
    _see_also = ['conflicts']
75
93
    def run(self, file_list=None, all=False):
76
94
        from bzrlib.workingtree import WorkingTree
77
95
        if all:
78
96
            if file_list:
79
 
                raise BzrCommandError("If --all is specified, no FILE may be provided")
 
97
                raise errors.BzrCommandError("If --all is specified,"
 
98
                                             " no FILE may be provided")
80
99
            tree = WorkingTree.open_containing('.')[0]
81
100
            resolve(tree)
82
101
        else:
 
102
            tree, file_list = builtins.tree_files(file_list)
83
103
            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()
 
104
                un_resolved, resolved = tree.auto_resolve()
 
105
                if len(un_resolved) > 0:
 
106
                    trace.note('%d conflict(s) auto-resolved.', len(resolved))
 
107
                    trace.note('Remaining conflicts:')
 
108
                    for conflict in un_resolved:
 
109
                        trace.note(conflict)
 
110
                    return 1
 
111
                else:
 
112
                    trace.note('All conflicts resolved.')
 
113
                    return 0
 
114
            else:
 
115
                resolve(tree, file_list)
 
116
 
 
117
 
 
118
def resolve(tree, paths=None, ignore_misses=False, recursive=False):
 
119
    """Resolve some or all of the conflicts in a working tree.
 
120
 
 
121
    :param paths: If None, resolve all conflicts.  Otherwise, select only
 
122
        specified conflicts.
 
123
    :param recursive: If True, then elements of paths which are directories
 
124
        have all their children resolved, etc.  When invoked as part of
 
125
        recursive commands like revert, this should be True.  For commands
 
126
        or applications wishing finer-grained control, like the resolve
 
127
        command, this should be False.
 
128
    :ignore_misses: If False, warnings will be printed if the supplied paths
 
129
        do not have conflicts.
 
130
    """
 
131
    tree.lock_tree_write()
92
132
    try:
93
133
        tree_conflicts = tree.conflicts()
94
134
        if paths is None:
96
136
            selected_conflicts = tree_conflicts
97
137
        else:
98
138
            new_conflicts, selected_conflicts = \
99
 
                tree_conflicts.select_conflicts(tree, paths, ignore_misses)
 
139
                tree_conflicts.select_conflicts(tree, paths, ignore_misses,
 
140
                    recursive)
100
141
        try:
101
142
            tree.set_conflicts(new_conflicts)
102
 
        except UnsupportedOperation:
 
143
        except errors.UnsupportedOperation:
103
144
            pass
104
145
        selected_conflicts.remove_files(tree)
105
146
    finally:
107
148
 
108
149
 
109
150
def restore(filename):
110
 
    """\
111
 
    Restore a conflicted file to the state it was in before merging.
112
 
    Only text restoration supported at present.
 
151
    """Restore a conflicted file to the state it was in before merging.
 
152
 
 
153
    Only text restoration is supported at present.
113
154
    """
114
155
    conflicted = False
115
156
    try:
116
 
        rename(filename + ".THIS", filename)
 
157
        osutils.rename(filename + ".THIS", filename)
117
158
        conflicted = True
118
159
    except OSError, e:
119
160
        if e.errno != errno.ENOENT:
131
172
        if e.errno != errno.ENOENT:
132
173
            raise
133
174
    if not conflicted:
134
 
        raise NotConflicted(filename)
 
175
        raise errors.NotConflicted(filename)
135
176
 
136
177
 
137
178
class ConflictList(object):
185
226
        """Generator of stanzas"""
186
227
        for conflict in self:
187
228
            yield conflict.as_stanza()
188
 
            
 
229
 
189
230
    def to_strings(self):
190
231
        """Generate strings for the provided conflicts"""
191
232
        for conflict in self:
198
239
                continue
199
240
            for suffix in CONFLICT_SUFFIXES:
200
241
                try:
201
 
                    delete_any(tree.abspath(conflict.path+suffix))
 
242
                    osutils.delete_any(tree.abspath(conflict.path+suffix))
202
243
                except OSError, e:
203
244
                    if e.errno != errno.ENOENT:
204
245
                        raise
205
246
 
206
 
    def select_conflicts(self, tree, paths, ignore_misses=False):
 
247
    def select_conflicts(self, tree, paths, ignore_misses=False,
 
248
                         recurse=False):
207
249
        """Select the conflicts associated with paths in a tree.
208
 
        
 
250
 
209
251
        File-ids are also used for this.
210
252
        :return: a pair of ConflictLists: (not_selected, selected)
211
253
        """
228
270
                if cpath in path_set:
229
271
                    selected = True
230
272
                    selected_paths.add(cpath)
 
273
                if recurse:
 
274
                    if osutils.is_inside_any(path_set, cpath):
 
275
                        selected = True
 
276
                        selected_paths.add(cpath)
 
277
 
231
278
            for key in ('file_id', 'conflict_file_id'):
232
279
                cfile_id = getattr(conflict, key, None)
233
280
                if cfile_id is None:
250
297
                    print "%s is not conflicted" % path
251
298
        return new_conflicts, selected_conflicts
252
299
 
253
 
 
 
300
 
254
301
class Conflict(object):
255
302
    """Base class for all types of conflict"""
256
303
 
258
305
 
259
306
    def __init__(self, path, file_id=None):
260
307
        self.path = path
261
 
        self.file_id = file_id
 
308
        # warn turned off, because the factory blindly transfers the Stanza
 
309
        # values to __init__ and Stanza is purely a Unicode api.
 
310
        self.file_id = osutils.safe_file_id(file_id, warn=False)
262
311
 
263
312
    def as_stanza(self):
264
 
        s = Stanza(type=self.typestring, path=self.path)
 
313
        s = rio.Stanza(type=self.typestring, path=self.path)
265
314
        if self.file_id is not None:
266
 
            s.add('file_id', self.file_id)
 
315
            # Stanza requires Unicode apis
 
316
            s.add('file_id', self.file_id.decode('utf8'))
267
317
        return s
268
318
 
269
319
    def _cmp_list(self):
351
401
    """
352
402
 
353
403
    rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
354
 
    
 
404
 
355
405
    def __init__(self, action, path, file_id=None):
356
406
        Conflict.__init__(self, path, file_id)
357
407
        self.action = action
376
426
    def __init__(self, action, path, conflict_path, file_id=None,
377
427
                 conflict_file_id=None):
378
428
        HandledConflict.__init__(self, action, path, file_id)
379
 
        self.conflict_path = conflict_path 
380
 
        self.conflict_file_id = conflict_file_id
381
 
        
 
429
        self.conflict_path = conflict_path
 
430
        # warn turned off, because the factory blindly transfers the Stanza
 
431
        # values to __init__.
 
432
        self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
 
433
                                                     warn=False)
 
434
 
382
435
    def _cmp_list(self):
383
 
        return HandledConflict._cmp_list(self) + [self.conflict_path, 
 
436
        return HandledConflict._cmp_list(self) + [self.conflict_path,
384
437
                                                  self.conflict_file_id]
385
438
 
386
439
    def as_stanza(self):
387
440
        s = HandledConflict.as_stanza(self)
388
441
        s.add('conflict_path', self.conflict_path)
389
442
        if self.conflict_file_id is not None:
390
 
            s.add('conflict_file_id', self.conflict_file_id)
391
 
            
 
443
            s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
 
444
 
392
445
        return s
393
446
 
394
447
 
425
478
 
426
479
 
427
480
class UnversionedParent(HandledConflict):
428
 
    """An attempt to version an file whose parent directory is not versioned.
 
481
    """An attempt to version a file whose parent directory is not versioned.
429
482
    Typically, the result of a merge where one tree unversioned the directory
430
483
    and the other added a versioned file to it.
431
484
    """
432
485
 
433
486
    typestring = 'unversioned parent'
434
487
 
435
 
    format = 'Conflict adding versioned files to %(path)s.  %(action)s.'
 
488
    format = 'Conflict because %(path)s is not versioned, but has versioned'\
 
489
             ' children.  %(action)s.'
436
490
 
437
491
 
438
492
class MissingParent(HandledConflict):
439
493
    """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.
 
494
    Typically, the result of a merge where THIS deleted the directory and
 
495
    the OTHER added a file to it.
 
496
    See also: DeletingParent (same situation, reversed THIS and OTHER)
442
497
    """
443
498
 
444
499
    typestring = 'missing parent'
446
501
    format = 'Conflict adding files to %(path)s.  %(action)s.'
447
502
 
448
503
 
 
504
class DeletingParent(HandledConflict):
 
505
    """An attempt to add files to a directory that is not present.
 
506
    Typically, the result of a merge where one OTHER deleted the directory and
 
507
    the THIS added a file to it.
 
508
    """
 
509
 
 
510
    typestring = 'deleting parent'
 
511
 
 
512
    format = "Conflict: can't delete %(path)s because it is not empty.  "\
 
513
             "%(action)s."
 
514
 
 
515
 
 
516
class NonDirectoryParent(HandledConflict):
 
517
    """An attempt to add files to a directory that is not a director or
 
518
    an attempt to change the kind of a directory with files.
 
519
    """
 
520
 
 
521
    typestring = 'non-directory parent'
 
522
 
 
523
    format = "Conflict: %(path)s is not a directory, but has files in it."\
 
524
             "  %(action)s."
449
525
 
450
526
ctype = {}
451
527
 
458
534
 
459
535
 
460
536
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
461
 
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)
 
537
               DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
 
538
               DeletingParent, NonDirectoryParent)