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(), """
37
from bzrlib.option import Option
40
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
43
class cmd_conflicts(commands.Command):
44
"""List files with conflicts.
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.
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.)
55
Use bzr resolve when you have fixed a problem.
61
help='List paths of files with text conflicts.'),
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():
69
if conflict.typestring != 'text conflict':
71
self.outf.write(conflict.path + '\n')
73
self.outf.write(str(conflict) + '\n')
76
class cmd_resolve(commands.Command):
77
"""Mark a conflict as resolved.
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.
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.
88
See also bzr conflicts.
90
aliases = ['resolved']
91
takes_args = ['file*']
93
Option('all', help='Resolve all conflicts in this tree.'),
95
def run(self, file_list=None, all=False):
96
from bzrlib.workingtree import WorkingTree
99
raise errors.BzrCommandError("If --all is specified,"
100
" no FILE may be provided")
101
tree = WorkingTree.open_containing('.')[0]
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:
114
trace.note('All conflicts resolved.')
117
resolve(tree, file_list)
120
def resolve(tree, paths=None, ignore_misses=False):
121
tree.lock_tree_write()
123
tree_conflicts = tree.conflicts()
125
new_conflicts = ConflictList()
126
selected_conflicts = tree_conflicts
128
new_conflicts, selected_conflicts = \
129
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
131
tree.set_conflicts(new_conflicts)
132
except errors.UnsupportedOperation:
134
selected_conflicts.remove_files(tree)
139
def restore(filename):
141
Restore a conflicted file to the state it was in before merging.
142
Only text restoration supported at present.
146
osutils.rename(filename + ".THIS", filename)
149
if e.errno != errno.ENOENT:
152
os.unlink(filename + ".BASE")
155
if e.errno != errno.ENOENT:
158
os.unlink(filename + ".OTHER")
161
if e.errno != errno.ENOENT:
164
raise errors.NotConflicted(filename)
167
class ConflictList(object):
168
"""List of conflicts.
170
Typically obtained from WorkingTree.conflicts()
172
Can be instantiated from stanzas or from Conflict subclasses.
175
def __init__(self, conflicts=None):
176
object.__init__(self)
177
if conflicts is None:
180
self.__list = conflicts
183
return len(self.__list) == 0
186
return len(self.__list)
189
return iter(self.__list)
191
def __getitem__(self, key):
192
return self.__list[key]
194
def append(self, conflict):
195
return self.__list.append(conflict)
197
def __eq__(self, other_list):
198
return list(self) == list(other_list)
200
def __ne__(self, other_list):
201
return not (self == other_list)
204
return "ConflictList(%r)" % self.__list
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()))
214
def to_stanzas(self):
215
"""Generator of stanzas"""
216
for conflict in self:
217
yield conflict.as_stanza()
219
def to_strings(self):
220
"""Generate strings for the provided conflicts"""
221
for conflict in self:
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:
229
for suffix in CONFLICT_SUFFIXES:
231
osutils.delete_any(tree.abspath(conflict.path+suffix))
233
if e.errno != errno.ENOENT:
236
def select_conflicts(self, tree, paths, ignore_misses=False,
238
"""Select the conflicts associated with paths in a tree.
240
File-ids are also used for this.
241
:return: a pair of ConflictLists: (not_selected, selected)
243
path_set = set(paths)
245
selected_paths = set()
246
new_conflicts = ConflictList()
247
selected_conflicts = ConflictList()
249
file_id = tree.path2id(path)
250
if file_id is not None:
253
for conflict in self:
255
for key in ('path', 'conflict_path'):
256
cpath = getattr(conflict, key, None)
259
if cpath in path_set:
261
selected_paths.add(cpath)
263
if osutils.is_inside_any(path_set, cpath):
265
selected_paths.add(cpath)
267
for key in ('file_id', 'conflict_file_id'):
268
cfile_id = getattr(conflict, key, None)
272
cpath = ids[cfile_id]
276
selected_paths.add(cpath)
278
selected_conflicts.append(conflict)
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
286
print "%s is not conflicted" % path
287
return new_conflicts, selected_conflicts
290
class Conflict(object):
291
"""Base class for all types of conflict"""
295
def __init__(self, path, file_id=None):
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)
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'))
309
return [type(self), self.path, self.file_id]
311
def __cmp__(self, other):
312
if getattr(other, "_cmp_list", None) is None:
314
return cmp(self._cmp_list(), other._cmp_list())
317
return hash((type(self), self.path, self.file_id))
319
def __eq__(self, other):
320
return self.__cmp__(other) == 0
322
def __ne__(self, other):
323
return not self.__eq__(other)
326
return self.format % self.__dict__
329
rdict = dict(self.__dict__)
330
rdict['class'] = self.__class__.__name__
331
return self.rformat % rdict
334
def factory(type, **kwargs):
336
return ctype[type](**kwargs)
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
345
return None, conflict.typestring
348
class PathConflict(Conflict):
349
"""A conflict was encountered merging file paths"""
351
typestring = 'path conflict'
353
format = 'Path conflict: %(path)s / %(conflict_path)s'
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
361
s = Conflict.as_stanza(self)
362
if self.conflict_path is not None:
363
s.add('conflict_path', self.conflict_path)
367
class ContentsConflict(PathConflict):
368
"""The files are of different types, or not present"""
372
typestring = 'contents conflict'
374
format = 'Contents conflict in %(path)s'
377
class TextConflict(PathConflict):
378
"""The merge algorithm could not resolve all differences encountered."""
382
typestring = 'text conflict'
384
format = 'Text conflict in %(path)s'
387
class HandledConflict(Conflict):
388
"""A path problem that has been provisionally resolved.
389
This is intended to be a base class.
392
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
394
def __init__(self, action, path, file_id=None):
395
Conflict.__init__(self, path, file_id)
399
return Conflict._cmp_list(self) + [self.action]
402
s = Conflict.as_stanza(self)
403
s.add('action', self.action)
407
class HandledPathConflict(HandledConflict):
408
"""A provisionally-resolved path problem involving two paths.
409
This is intended to be a base class.
412
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
413
" %(file_id)r, %(conflict_file_id)r)"
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,
425
return HandledConflict._cmp_list(self) + [self.conflict_path,
426
self.conflict_file_id]
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'))
437
class DuplicateID(HandledPathConflict):
438
"""Two files want the same file_id."""
440
typestring = 'duplicate id'
442
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
445
class DuplicateEntry(HandledPathConflict):
446
"""Two directory entries want to have the same name."""
448
typestring = 'duplicate'
450
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
453
class ParentLoop(HandledPathConflict):
454
"""An attempt to create an infinitely-looping directory structure.
455
This is rare, but can be produced like so:
464
typestring = 'parent loop'
466
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
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.
475
typestring = 'unversioned parent'
477
format = 'Conflict because %(path)s is not versioned, but has versioned'\
478
' children. %(action)s.'
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)
488
typestring = 'missing parent'
490
format = 'Conflict adding files to %(path)s. %(action)s.'
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.
499
typestring = 'deleting parent'
501
format = "Conflict: can't delete %(path)s because it is not empty. "\
508
def register_types(*conflict_types):
509
"""Register a Conflict subclass for serialization purposes"""
511
for conflict_type in conflict_types:
512
ctype[conflict_type.typestring] = conflict_type
515
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
516
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,