~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/conflicts.py

- refactor handling of short option names

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
 
22
25
import os
23
26
import errno
24
27
 
25
 
import bzrlib
 
28
import bzrlib.status
 
29
from bzrlib.branch import Branch
 
30
from bzrlib.errors import BzrCommandError
26
31
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
 
 
 
32
from bzrlib.workingtree import CONFLICT_SUFFIXES
35
33
 
36
34
class cmd_conflicts(bzrlib.commands.Command):
37
35
    """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
36
    (conflicts are determined by the presence of .BASE .TREE, and .OTHER 
47
37
    files.)
48
 
 
49
 
    See also bzr resolve.
50
38
    """
51
39
    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
 
 
 
40
        for path in Branch.open_containing('.').working_tree().iter_conflicts():
 
41
            print path
57
42
 
58
43
class cmd_resolve(bzrlib.commands.Command):
59
44
    """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
45
    """
72
 
    aliases = ['resolved']
73
46
    takes_args = ['file*']
74
 
    takes_options = [Option('all', help='Resolve all conflicts in this tree')]
 
47
    takes_options = ['all']
75
48
    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
 
49
        if file_list is None:
 
50
            if not all:
 
51
                raise BzrCommandError(
 
52
                    "command 'resolve' needs one or more FILE, or --all")
 
53
            tree = Branch.open_containing('.').working_tree()
 
54
            file_list = list(tree.abspath(f) for f in tree.iter_conflicts())
 
55
        else:
 
56
            if all:
 
57
                raise BzrCommandError(
 
58
                    "If --all is specified, no FILE may be provided")
 
59
        for filename in file_list:
 
60
            failures = 0
199
61
            for suffix in CONFLICT_SUFFIXES:
200
62
                try:
201
 
                    delete_any(tree.abspath(conflict.path+suffix))
 
63
                    os.unlink(filename+suffix)
202
64
                except OSError, e:
203
65
                    if e.errno != errno.ENOENT:
204
66
                        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
 
67
                    else:
 
68
                        failures += 1
 
69
            if failures == len(CONFLICT_SUFFIXES):
 
70
                if not os.path.exists(filename):
 
71
                    print "%s does not exist" % filename
249
72
                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,)
 
73
                    print "%s is not conflicted" % filename