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.
59
takes_options = [Option('text', help='list text conflicts by pathname')]
61
def run(self, text=False):
62
from bzrlib.workingtree import WorkingTree
63
wt = WorkingTree.open_containing(u'.')[0]
64
for conflict in wt.conflicts():
66
if conflict.typestring != 'text conflict':
68
self.outf.write(conflict.path + '\n')
70
self.outf.write(str(conflict) + '\n')
73
class cmd_resolve(commands.Command):
74
"""Mark a conflict as resolved.
76
Merge will do its best to combine the changes in two branches, but there
77
are some kinds of problems only a human can fix. When it encounters those,
78
it will mark a conflict. A conflict means that you need to fix something,
79
before you should commit.
81
Once you have fixed a problem, use "bzr resolve" to automatically mark
82
text conflicts as fixed, resolve FILE to mark a specific conflict as
83
resolved, or "bzr resolve --all" to mark all conflicts as resolved.
85
See also bzr conflicts.
87
aliases = ['resolved']
88
takes_args = ['file*']
89
takes_options = [Option('all', help='Resolve all conflicts in this tree')]
90
def run(self, file_list=None, all=False):
91
from bzrlib.workingtree import WorkingTree
94
raise errors.BzrCommandError("If --all is specified,"
95
" no FILE may be provided")
96
tree = WorkingTree.open_containing('.')[0]
99
tree, file_list = builtins.tree_files(file_list)
100
if file_list is None:
101
un_resolved, resolved = tree.auto_resolve()
102
if len(un_resolved) > 0:
103
trace.note('%d conflict(s) auto-resolved.', len(resolved))
104
trace.note('Remaining conflicts:')
105
for conflict in un_resolved:
109
trace.note('All conflicts resolved.')
112
resolve(tree, file_list)
115
def resolve(tree, paths=None, ignore_misses=False):
116
tree.lock_tree_write()
118
tree_conflicts = tree.conflicts()
120
new_conflicts = ConflictList()
121
selected_conflicts = tree_conflicts
123
new_conflicts, selected_conflicts = \
124
tree_conflicts.select_conflicts(tree, paths, ignore_misses)
126
tree.set_conflicts(new_conflicts)
127
except errors.UnsupportedOperation:
129
selected_conflicts.remove_files(tree)
134
def restore(filename):
136
Restore a conflicted file to the state it was in before merging.
137
Only text restoration supported at present.
141
osutils.rename(filename + ".THIS", filename)
144
if e.errno != errno.ENOENT:
147
os.unlink(filename + ".BASE")
150
if e.errno != errno.ENOENT:
153
os.unlink(filename + ".OTHER")
156
if e.errno != errno.ENOENT:
159
raise errors.NotConflicted(filename)
162
class ConflictList(object):
163
"""List of conflicts.
165
Typically obtained from WorkingTree.conflicts()
167
Can be instantiated from stanzas or from Conflict subclasses.
170
def __init__(self, conflicts=None):
171
object.__init__(self)
172
if conflicts is None:
175
self.__list = conflicts
178
return len(self.__list) == 0
181
return len(self.__list)
184
return iter(self.__list)
186
def __getitem__(self, key):
187
return self.__list[key]
189
def append(self, conflict):
190
return self.__list.append(conflict)
192
def __eq__(self, other_list):
193
return list(self) == list(other_list)
195
def __ne__(self, other_list):
196
return not (self == other_list)
199
return "ConflictList(%r)" % self.__list
202
def from_stanzas(stanzas):
203
"""Produce a new ConflictList from an iterable of stanzas"""
204
conflicts = ConflictList()
205
for stanza in stanzas:
206
conflicts.append(Conflict.factory(**stanza.as_dict()))
209
def to_stanzas(self):
210
"""Generator of stanzas"""
211
for conflict in self:
212
yield conflict.as_stanza()
214
def to_strings(self):
215
"""Generate strings for the provided conflicts"""
216
for conflict in self:
219
def remove_files(self, tree):
220
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
221
for conflict in self:
222
if not conflict.has_files:
224
for suffix in CONFLICT_SUFFIXES:
226
osutils.delete_any(tree.abspath(conflict.path+suffix))
228
if e.errno != errno.ENOENT:
231
def select_conflicts(self, tree, paths, ignore_misses=False):
232
"""Select the conflicts associated with paths in a tree.
234
File-ids are also used for this.
235
:return: a pair of ConflictLists: (not_selected, selected)
237
path_set = set(paths)
239
selected_paths = set()
240
new_conflicts = ConflictList()
241
selected_conflicts = ConflictList()
243
file_id = tree.path2id(path)
244
if file_id is not None:
247
for conflict in self:
249
for key in ('path', 'conflict_path'):
250
cpath = getattr(conflict, key, None)
253
if cpath in path_set:
255
selected_paths.add(cpath)
256
for key in ('file_id', 'conflict_file_id'):
257
cfile_id = getattr(conflict, key, None)
261
cpath = ids[cfile_id]
265
selected_paths.add(cpath)
267
selected_conflicts.append(conflict)
269
new_conflicts.append(conflict)
270
if ignore_misses is not True:
271
for path in [p for p in paths if p not in selected_paths]:
272
if not os.path.exists(tree.abspath(path)):
273
print "%s does not exist" % path
275
print "%s is not conflicted" % path
276
return new_conflicts, selected_conflicts
279
class Conflict(object):
280
"""Base class for all types of conflict"""
284
def __init__(self, path, file_id=None):
286
# warn turned off, because the factory blindly transfers the Stanza
287
# values to __init__ and Stanza is purely a Unicode api.
288
self.file_id = osutils.safe_file_id(file_id, warn=False)
291
s = rio.Stanza(type=self.typestring, path=self.path)
292
if self.file_id is not None:
293
# Stanza requires Unicode apis
294
s.add('file_id', self.file_id.decode('utf8'))
298
return [type(self), self.path, self.file_id]
300
def __cmp__(self, other):
301
if getattr(other, "_cmp_list", None) is None:
303
return cmp(self._cmp_list(), other._cmp_list())
306
return hash((type(self), self.path, self.file_id))
308
def __eq__(self, other):
309
return self.__cmp__(other) == 0
311
def __ne__(self, other):
312
return not self.__eq__(other)
315
return self.format % self.__dict__
318
rdict = dict(self.__dict__)
319
rdict['class'] = self.__class__.__name__
320
return self.rformat % rdict
323
def factory(type, **kwargs):
325
return ctype[type](**kwargs)
328
def sort_key(conflict):
329
if conflict.path is not None:
330
return conflict.path, conflict.typestring
331
elif getattr(conflict, "conflict_path", None) is not None:
332
return conflict.conflict_path, conflict.typestring
334
return None, conflict.typestring
337
class PathConflict(Conflict):
338
"""A conflict was encountered merging file paths"""
340
typestring = 'path conflict'
342
format = 'Path conflict: %(path)s / %(conflict_path)s'
344
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
345
def __init__(self, path, conflict_path=None, file_id=None):
346
Conflict.__init__(self, path, file_id)
347
self.conflict_path = conflict_path
350
s = Conflict.as_stanza(self)
351
if self.conflict_path is not None:
352
s.add('conflict_path', self.conflict_path)
356
class ContentsConflict(PathConflict):
357
"""The files are of different types, or not present"""
361
typestring = 'contents conflict'
363
format = 'Contents conflict in %(path)s'
366
class TextConflict(PathConflict):
367
"""The merge algorithm could not resolve all differences encountered."""
371
typestring = 'text conflict'
373
format = 'Text conflict in %(path)s'
376
class HandledConflict(Conflict):
377
"""A path problem that has been provisionally resolved.
378
This is intended to be a base class.
381
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
383
def __init__(self, action, path, file_id=None):
384
Conflict.__init__(self, path, file_id)
388
return Conflict._cmp_list(self) + [self.action]
391
s = Conflict.as_stanza(self)
392
s.add('action', self.action)
396
class HandledPathConflict(HandledConflict):
397
"""A provisionally-resolved path problem involving two paths.
398
This is intended to be a base class.
401
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
402
" %(file_id)r, %(conflict_file_id)r)"
404
def __init__(self, action, path, conflict_path, file_id=None,
405
conflict_file_id=None):
406
HandledConflict.__init__(self, action, path, file_id)
407
self.conflict_path = conflict_path
408
# warn turned off, because the factory blindly transfers the Stanza
409
# values to __init__.
410
self.conflict_file_id = osutils.safe_file_id(conflict_file_id,
414
return HandledConflict._cmp_list(self) + [self.conflict_path,
415
self.conflict_file_id]
418
s = HandledConflict.as_stanza(self)
419
s.add('conflict_path', self.conflict_path)
420
if self.conflict_file_id is not None:
421
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
426
class DuplicateID(HandledPathConflict):
427
"""Two files want the same file_id."""
429
typestring = 'duplicate id'
431
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
434
class DuplicateEntry(HandledPathConflict):
435
"""Two directory entries want to have the same name."""
437
typestring = 'duplicate'
439
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
442
class ParentLoop(HandledPathConflict):
443
"""An attempt to create an infinitely-looping directory structure.
444
This is rare, but can be produced like so:
453
typestring = 'parent loop'
455
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
458
class UnversionedParent(HandledConflict):
459
"""An attempt to version an file whose parent directory is not versioned.
460
Typically, the result of a merge where one tree unversioned the directory
461
and the other added a versioned file to it.
464
typestring = 'unversioned parent'
466
format = 'Conflict because %(path)s is not versioned, but has versioned'\
467
' children. %(action)s.'
470
class MissingParent(HandledConflict):
471
"""An attempt to add files to a directory that is not present.
472
Typically, the result of a merge where THIS deleted the directory and
473
the OTHER added a file to it.
474
See also: DeletingParent (same situation, reversed THIS and OTHER)
477
typestring = 'missing parent'
479
format = 'Conflict adding files to %(path)s. %(action)s.'
482
class DeletingParent(HandledConflict):
483
"""An attempt to add files to a directory that is not present.
484
Typically, the result of a merge where one OTHER deleted the directory and
485
the THIS added a file to it.
488
typestring = 'deleting parent'
490
format = "Conflict: can't delete %(path)s because it is not empty. "\
497
def register_types(*conflict_types):
498
"""Register a Conflict subclass for serialization purposes"""
500
for conflict_type in conflict_types:
501
ctype[conflict_type.typestring] = conflict_type
504
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
505
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,