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
Conflicts normally are listed as short, human-readable messages. If --text
50
is supplied, the pathnames of files with text conflicts are listed,
51
instead. (This is useful for editing all files with text conflicts.)
53
Use bzr resolve when you have fixed a problem.
57
takes_options = [Option('text', help='list text conflicts by pathname')]
59
def run(self, text=False):
60
from bzrlib.workingtree import WorkingTree
61
wt = WorkingTree.open_containing(u'.')[0]
62
for conflict in wt.conflicts():
64
if conflict.typestring != 'text conflict':
66
self.outf.write(conflict.path + '\n')
68
self.outf.write(str(conflict) + '\n')
71
class cmd_resolve(commands.Command):
72
"""Mark a conflict as resolved.
74
Merge will do its best to combine the changes in two branches, but there
75
are some kinds of problems only a human can fix. When it encounters those,
76
it will mark a conflict. A conflict means that you need to fix something,
77
before you should commit.
79
Once you have fixed a problem, use "bzr resolve FILE.." to mark
80
individual files as fixed, or "bzr resolve --all" to mark all conflicts as
83
See also bzr conflicts.
85
aliases = ['resolved']
86
takes_args = ['file*']
87
takes_options = [Option('all', help='Resolve all conflicts in this tree')]
88
def run(self, file_list=None, all=False):
89
from bzrlib.workingtree import WorkingTree
92
raise errors.BzrCommandError("If --all is specified,"
93
" no FILE may be provided")
94
tree = WorkingTree.open_containing('.')[0]
98
raise errors.BzrCommandError("command 'resolve' needs one or"
99
" more FILE, or --all")
100
tree = WorkingTree.open_containing(file_list[0])[0]
101
to_resolve = [tree.relpath(p) for p in file_list]
102
resolve(tree, to_resolve)
105
def resolve(tree, paths=None, ignore_misses=False):
106
tree.lock_tree_write()
108
tree_conflicts = tree.conflicts()
110
new_conflicts = ConflictList()
111
selected_conflicts = tree_conflicts
113
new_conflicts, selected_conflicts = \
114
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
116
tree.set_conflicts(new_conflicts)
117
except errors.UnsupportedOperation:
119
selected_conflicts.remove_files(tree)
124
def restore(filename):
126
Restore a conflicted file to the state it was in before merging.
127
Only text restoration supported at present.
131
osutils.rename(filename + ".THIS", filename)
134
if e.errno != errno.ENOENT:
137
os.unlink(filename + ".BASE")
140
if e.errno != errno.ENOENT:
143
os.unlink(filename + ".OTHER")
146
if e.errno != errno.ENOENT:
149
raise errors.NotConflicted(filename)
152
class ConflictList(object):
153
"""List of conflicts.
155
Typically obtained from WorkingTree.conflicts()
157
Can be instantiated from stanzas or from Conflict subclasses.
160
def __init__(self, conflicts=None):
161
object.__init__(self)
162
if conflicts is None:
165
self.__list = conflicts
168
return len(self.__list) == 0
171
return len(self.__list)
174
return iter(self.__list)
176
def __getitem__(self, key):
177
return self.__list[key]
179
def append(self, conflict):
180
return self.__list.append(conflict)
182
def __eq__(self, other_list):
183
return list(self) == list(other_list)
185
def __ne__(self, other_list):
186
return not (self == other_list)
189
return "ConflictList(%r)" % self.__list
192
def from_stanzas(stanzas):
193
"""Produce a new ConflictList from an iterable of stanzas"""
194
conflicts = ConflictList()
195
for stanza in stanzas:
196
conflicts.append(Conflict.factory(**stanza.as_dict()))
199
def to_stanzas(self):
200
"""Generator of stanzas"""
201
for conflict in self:
202
yield conflict.as_stanza()
204
def to_strings(self):
205
"""Generate strings for the provided conflicts"""
206
for conflict in self:
209
def remove_files(self, tree):
210
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
211
for conflict in self:
212
if not conflict.has_files:
214
for suffix in CONFLICT_SUFFIXES:
216
osutils.delete_any(tree.abspath(conflict.path+suffix))
218
if e.errno != errno.ENOENT:
221
def select_conflicts(self, tree, paths, ignore_misses=False):
222
"""Select the conflicts associated with paths in a tree.
224
File-ids are also used for this.
225
:return: a pair of ConflictLists: (not_selected, selected)
227
path_set = set(paths)
229
selected_paths = set()
230
new_conflicts = ConflictList()
231
selected_conflicts = ConflictList()
233
file_id = tree.path2id(path)
234
if file_id is not None:
237
for conflict in self:
239
for key in ('path', 'conflict_path'):
240
cpath = getattr(conflict, key, None)
243
if cpath in path_set:
245
selected_paths.add(cpath)
246
for key in ('file_id', 'conflict_file_id'):
247
cfile_id = getattr(conflict, key, None)
251
cpath = ids[cfile_id]
255
selected_paths.add(cpath)
257
selected_conflicts.append(conflict)
259
new_conflicts.append(conflict)
260
if ignore_misses is not True:
261
for path in [p for p in paths if p not in selected_paths]:
262
if not os.path.exists(tree.abspath(path)):
263
print "%s does not exist" % path
265
print "%s is not conflicted" % path
266
return new_conflicts, selected_conflicts
269
class Conflict(object):
270
"""Base class for all types of conflict"""
274
def __init__(self, path, file_id=None):
276
self.file_id = file_id
279
s = rio.Stanza(type=self.typestring, path=self.path)
280
if self.file_id is not None:
281
s.add('file_id', self.file_id)
285
return [type(self), self.path, self.file_id]
287
def __cmp__(self, other):
288
if getattr(other, "_cmp_list", None) is None:
290
return cmp(self._cmp_list(), other._cmp_list())
293
return hash((type(self), self.path, self.file_id))
295
def __eq__(self, other):
296
return self.__cmp__(other) == 0
298
def __ne__(self, other):
299
return not self.__eq__(other)
302
return self.format % self.__dict__
305
rdict = dict(self.__dict__)
306
rdict['class'] = self.__class__.__name__
307
return self.rformat % rdict
310
def factory(type, **kwargs):
312
return ctype[type](**kwargs)
315
def sort_key(conflict):
316
if conflict.path is not None:
317
return conflict.path, conflict.typestring
318
elif getattr(conflict, "conflict_path", None) is not None:
319
return conflict.conflict_path, conflict.typestring
321
return None, conflict.typestring
324
class PathConflict(Conflict):
325
"""A conflict was encountered merging file paths"""
327
typestring = 'path conflict'
329
format = 'Path conflict: %(path)s / %(conflict_path)s'
331
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
332
def __init__(self, path, conflict_path=None, file_id=None):
333
Conflict.__init__(self, path, file_id)
334
self.conflict_path = conflict_path
337
s = Conflict.as_stanza(self)
338
if self.conflict_path is not None:
339
s.add('conflict_path', self.conflict_path)
343
class ContentsConflict(PathConflict):
344
"""The files are of different types, or not present"""
348
typestring = 'contents conflict'
350
format = 'Contents conflict in %(path)s'
353
class TextConflict(PathConflict):
354
"""The merge algorithm could not resolve all differences encountered."""
358
typestring = 'text conflict'
360
format = 'Text conflict in %(path)s'
363
class HandledConflict(Conflict):
364
"""A path problem that has been provisionally resolved.
365
This is intended to be a base class.
368
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
370
def __init__(self, action, path, file_id=None):
371
Conflict.__init__(self, path, file_id)
375
return Conflict._cmp_list(self) + [self.action]
378
s = Conflict.as_stanza(self)
379
s.add('action', self.action)
383
class HandledPathConflict(HandledConflict):
384
"""A provisionally-resolved path problem involving two paths.
385
This is intended to be a base class.
388
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
389
" %(file_id)r, %(conflict_file_id)r)"
391
def __init__(self, action, path, conflict_path, file_id=None,
392
conflict_file_id=None):
393
HandledConflict.__init__(self, action, path, file_id)
394
self.conflict_path = conflict_path
395
self.conflict_file_id = conflict_file_id
398
return HandledConflict._cmp_list(self) + [self.conflict_path,
399
self.conflict_file_id]
402
s = HandledConflict.as_stanza(self)
403
s.add('conflict_path', self.conflict_path)
404
if self.conflict_file_id is not None:
405
s.add('conflict_file_id', self.conflict_file_id)
410
class DuplicateID(HandledPathConflict):
411
"""Two files want the same file_id."""
413
typestring = 'duplicate id'
415
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
418
class DuplicateEntry(HandledPathConflict):
419
"""Two directory entries want to have the same name."""
421
typestring = 'duplicate'
423
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
426
class ParentLoop(HandledPathConflict):
427
"""An attempt to create an infinitely-looping directory structure.
428
This is rare, but can be produced like so:
437
typestring = 'parent loop'
439
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
442
class UnversionedParent(HandledConflict):
443
"""An attempt to version an file whose parent directory is not versioned.
444
Typically, the result of a merge where one tree unversioned the directory
445
and the other added a versioned file to it.
448
typestring = 'unversioned parent'
450
format = 'Conflict because %(path)s is not versioned, but has versioned'\
451
' children. %(action)s.'
454
class MissingParent(HandledConflict):
455
"""An attempt to add files to a directory that is not present.
456
Typically, the result of a merge where THIS deleted the directory and
457
the OTHER added a file to it.
458
See also: DeletingParent (same situation, reversed THIS and OTHER)
461
typestring = 'missing parent'
463
format = 'Conflict adding files to %(path)s. %(action)s.'
466
class DeletingParent(HandledConflict):
467
"""An attempt to add files to a directory that is not present.
468
Typically, the result of a merge where one OTHER deleted the directory and
469
the THIS added a file to it.
472
typestring = 'deleting parent'
474
format = "Conflict: can't delete %(path)s because it is not empty. "\
481
def register_types(*conflict_types):
482
"""Register a Conflict subclass for serialization purposes"""
484
for conflict_type in conflict_types:
485
ctype[conflict_type.typestring] = conflict_type
488
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
489
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,