1
# Copyright (C) 2005 Aaron Bentley, Canonical Ltd
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.
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.
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
17
# TODO: Move this into builtins
19
# TODO: 'bzr resolve' should accept a directory name and work from that
24
from bzrlib.lazy_import import lazy_import
25
lazy_import(globals(), """
35
from bzrlib.option import Option
38
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
41
class cmd_conflicts(commands.Command):
42
"""List files with conflicts.
44
Merge will do its best to combine the changes in two branches, but there
45
are some kinds of problems only a human can fix. When it encounters those,
46
it will mark a conflict. A conflict means that you need to fix something,
47
before you should commit.
49
Use bzr resolve when you have fixed a problem.
51
(conflicts are determined by the presence of .BASE .TREE, and .OTHER
57
from bzrlib.workingtree import WorkingTree
58
wt = WorkingTree.open_containing(u'.')[0]
59
for conflict in wt.conflicts():
63
class cmd_resolve(commands.Command):
64
"""Mark a conflict as resolved.
66
Merge will do its best to combine the changes in two branches, but there
67
are some kinds of problems only a human can fix. When it encounters those,
68
it will mark a conflict. A conflict means that you need to fix something,
69
before you should commit.
71
Once you have fixed a problem, use "bzr resolve FILE.." to mark
72
individual files as fixed, or "bzr resolve --all" to mark all conflicts as
75
See also bzr conflicts.
77
aliases = ['resolved']
78
takes_args = ['file*']
79
takes_options = [Option('all', help='Resolve all conflicts in this tree')]
80
def run(self, file_list=None, all=False):
81
from bzrlib.workingtree import WorkingTree
84
raise errors.BzrCommandError("If --all is specified,"
85
" no FILE may be provided")
86
tree = WorkingTree.open_containing('.')[0]
90
raise errors.BzrCommandError("command 'resolve' needs one or"
91
" more FILE, or --all")
92
tree = WorkingTree.open_containing(file_list[0])[0]
93
to_resolve = [tree.relpath(p) for p in file_list]
94
resolve(tree, to_resolve)
97
def resolve(tree, paths=None, ignore_misses=False):
98
tree.lock_tree_write()
100
tree_conflicts = tree.conflicts()
102
new_conflicts = ConflictList()
103
selected_conflicts = tree_conflicts
105
new_conflicts, selected_conflicts = \
106
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
108
tree.set_conflicts(new_conflicts)
109
except errors.UnsupportedOperation:
111
selected_conflicts.remove_files(tree)
116
def restore(filename):
118
Restore a conflicted file to the state it was in before merging.
119
Only text restoration supported at present.
123
osutils.rename(filename + ".THIS", filename)
126
if e.errno != errno.ENOENT:
129
os.unlink(filename + ".BASE")
132
if e.errno != errno.ENOENT:
135
os.unlink(filename + ".OTHER")
138
if e.errno != errno.ENOENT:
141
raise errors.NotConflicted(filename)
144
class ConflictList(object):
145
"""List of conflicts.
147
Typically obtained from WorkingTree.conflicts()
149
Can be instantiated from stanzas or from Conflict subclasses.
152
def __init__(self, conflicts=None):
153
object.__init__(self)
154
if conflicts is None:
157
self.__list = conflicts
160
return len(self.__list) == 0
163
return len(self.__list)
166
return iter(self.__list)
168
def __getitem__(self, key):
169
return self.__list[key]
171
def append(self, conflict):
172
return self.__list.append(conflict)
174
def __eq__(self, other_list):
175
return list(self) == list(other_list)
177
def __ne__(self, other_list):
178
return not (self == other_list)
181
return "ConflictList(%r)" % self.__list
184
def from_stanzas(stanzas):
185
"""Produce a new ConflictList from an iterable of stanzas"""
186
conflicts = ConflictList()
187
for stanza in stanzas:
188
conflicts.append(Conflict.factory(**stanza.as_dict()))
191
def to_stanzas(self):
192
"""Generator of stanzas"""
193
for conflict in self:
194
yield conflict.as_stanza()
196
def to_strings(self):
197
"""Generate strings for the provided conflicts"""
198
for conflict in self:
201
def remove_files(self, tree):
202
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
203
for conflict in self:
204
if not conflict.has_files:
206
for suffix in CONFLICT_SUFFIXES:
208
osutils.delete_any(tree.abspath(conflict.path+suffix))
210
if e.errno != errno.ENOENT:
213
def select_conflicts(self, tree, paths, ignore_misses=False):
214
"""Select the conflicts associated with paths in a tree.
216
File-ids are also used for this.
217
:return: a pair of ConflictLists: (not_selected, selected)
219
path_set = set(paths)
221
selected_paths = set()
222
new_conflicts = ConflictList()
223
selected_conflicts = ConflictList()
225
file_id = tree.path2id(path)
226
if file_id is not None:
229
for conflict in self:
231
for key in ('path', 'conflict_path'):
232
cpath = getattr(conflict, key, None)
235
if cpath in path_set:
237
selected_paths.add(cpath)
238
for key in ('file_id', 'conflict_file_id'):
239
cfile_id = getattr(conflict, key, None)
243
cpath = ids[cfile_id]
247
selected_paths.add(cpath)
249
selected_conflicts.append(conflict)
251
new_conflicts.append(conflict)
252
if ignore_misses is not True:
253
for path in [p for p in paths if p not in selected_paths]:
254
if not os.path.exists(tree.abspath(path)):
255
print "%s does not exist" % path
257
print "%s is not conflicted" % path
258
return new_conflicts, selected_conflicts
261
class Conflict(object):
262
"""Base class for all types of conflict"""
266
def __init__(self, path, file_id=None):
268
self.file_id = file_id
271
s = rio.Stanza(type=self.typestring, path=self.path)
272
if self.file_id is not None:
273
s.add('file_id', self.file_id)
277
return [type(self), self.path, self.file_id]
279
def __cmp__(self, other):
280
if getattr(other, "_cmp_list", None) is None:
282
return cmp(self._cmp_list(), other._cmp_list())
285
return hash((type(self), self.path, self.file_id))
287
def __eq__(self, other):
288
return self.__cmp__(other) == 0
290
def __ne__(self, other):
291
return not self.__eq__(other)
294
return self.format % self.__dict__
297
rdict = dict(self.__dict__)
298
rdict['class'] = self.__class__.__name__
299
return self.rformat % rdict
302
def factory(type, **kwargs):
304
return ctype[type](**kwargs)
307
def sort_key(conflict):
308
if conflict.path is not None:
309
return conflict.path, conflict.typestring
310
elif getattr(conflict, "conflict_path", None) is not None:
311
return conflict.conflict_path, conflict.typestring
313
return None, conflict.typestring
316
class PathConflict(Conflict):
317
"""A conflict was encountered merging file paths"""
319
typestring = 'path conflict'
321
format = 'Path conflict: %(path)s / %(conflict_path)s'
323
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
324
def __init__(self, path, conflict_path=None, file_id=None):
325
Conflict.__init__(self, path, file_id)
326
self.conflict_path = conflict_path
329
s = Conflict.as_stanza(self)
330
if self.conflict_path is not None:
331
s.add('conflict_path', self.conflict_path)
335
class ContentsConflict(PathConflict):
336
"""The files are of different types, or not present"""
340
typestring = 'contents conflict'
342
format = 'Contents conflict in %(path)s'
345
class TextConflict(PathConflict):
346
"""The merge algorithm could not resolve all differences encountered."""
350
typestring = 'text conflict'
352
format = 'Text conflict in %(path)s'
355
class HandledConflict(Conflict):
356
"""A path problem that has been provisionally resolved.
357
This is intended to be a base class.
360
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
362
def __init__(self, action, path, file_id=None):
363
Conflict.__init__(self, path, file_id)
367
return Conflict._cmp_list(self) + [self.action]
370
s = Conflict.as_stanza(self)
371
s.add('action', self.action)
375
class HandledPathConflict(HandledConflict):
376
"""A provisionally-resolved path problem involving two paths.
377
This is intended to be a base class.
380
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
381
" %(file_id)r, %(conflict_file_id)r)"
383
def __init__(self, action, path, conflict_path, file_id=None,
384
conflict_file_id=None):
385
HandledConflict.__init__(self, action, path, file_id)
386
self.conflict_path = conflict_path
387
self.conflict_file_id = conflict_file_id
390
return HandledConflict._cmp_list(self) + [self.conflict_path,
391
self.conflict_file_id]
394
s = HandledConflict.as_stanza(self)
395
s.add('conflict_path', self.conflict_path)
396
if self.conflict_file_id is not None:
397
s.add('conflict_file_id', self.conflict_file_id)
402
class DuplicateID(HandledPathConflict):
403
"""Two files want the same file_id."""
405
typestring = 'duplicate id'
407
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
410
class DuplicateEntry(HandledPathConflict):
411
"""Two directory entries want to have the same name."""
413
typestring = 'duplicate'
415
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
418
class ParentLoop(HandledPathConflict):
419
"""An attempt to create an infinitely-looping directory structure.
420
This is rare, but can be produced like so:
429
typestring = 'parent loop'
431
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
434
class UnversionedParent(HandledConflict):
435
"""An attempt to version an file whose parent directory is not versioned.
436
Typically, the result of a merge where one tree unversioned the directory
437
and the other added a versioned file to it.
440
typestring = 'unversioned parent'
442
format = 'Conflict because %(path)s is not versioned, but has versioned'\
443
' children. %(action)s.'
446
class MissingParent(HandledConflict):
447
"""An attempt to add files to a directory that is not present.
448
Typically, the result of a merge where THIS deleted the directory and
449
the OTHER added a file to it.
450
See also: DeletingParent (same situation, reversed THIS and OTHER)
453
typestring = 'missing parent'
455
format = 'Conflict adding files to %(path)s. %(action)s.'
458
class DeletingParent(HandledConflict):
459
"""An attempt to add files to a directory that is not present.
460
Typically, the result of a merge where one OTHER deleted the directory and
461
the THIS added a file to it.
464
typestring = 'deleting parent'
466
format = "Conflict: can't delete %(path)s because it is not empty. "\
473
def register_types(*conflict_types):
474
"""Register a Conflict subclass for serialization purposes"""
476
for conflict_type in conflict_types:
477
ctype[conflict_type.typestring] = conflict_type
480
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
481
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,