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
self.file_id = file_id
289
s = rio.Stanza(type=self.typestring, path=self.path)
290
if self.file_id is not None:
291
s.add('file_id', self.file_id)
295
return [type(self), self.path, self.file_id]
297
def __cmp__(self, other):
298
if getattr(other, "_cmp_list", None) is None:
300
return cmp(self._cmp_list(), other._cmp_list())
303
return hash((type(self), self.path, self.file_id))
305
def __eq__(self, other):
306
return self.__cmp__(other) == 0
308
def __ne__(self, other):
309
return not self.__eq__(other)
312
return self.format % self.__dict__
315
rdict = dict(self.__dict__)
316
rdict['class'] = self.__class__.__name__
317
return self.rformat % rdict
320
def factory(type, **kwargs):
322
return ctype[type](**kwargs)
325
def sort_key(conflict):
326
if conflict.path is not None:
327
return conflict.path, conflict.typestring
328
elif getattr(conflict, "conflict_path", None) is not None:
329
return conflict.conflict_path, conflict.typestring
331
return None, conflict.typestring
334
class PathConflict(Conflict):
335
"""A conflict was encountered merging file paths"""
337
typestring = 'path conflict'
339
format = 'Path conflict: %(path)s / %(conflict_path)s'
341
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
342
def __init__(self, path, conflict_path=None, file_id=None):
343
Conflict.__init__(self, path, file_id)
344
self.conflict_path = conflict_path
347
s = Conflict.as_stanza(self)
348
if self.conflict_path is not None:
349
s.add('conflict_path', self.conflict_path)
353
class ContentsConflict(PathConflict):
354
"""The files are of different types, or not present"""
358
typestring = 'contents conflict'
360
format = 'Contents conflict in %(path)s'
363
class TextConflict(PathConflict):
364
"""The merge algorithm could not resolve all differences encountered."""
368
typestring = 'text conflict'
370
format = 'Text conflict in %(path)s'
373
class HandledConflict(Conflict):
374
"""A path problem that has been provisionally resolved.
375
This is intended to be a base class.
378
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
380
def __init__(self, action, path, file_id=None):
381
Conflict.__init__(self, path, file_id)
385
return Conflict._cmp_list(self) + [self.action]
388
s = Conflict.as_stanza(self)
389
s.add('action', self.action)
393
class HandledPathConflict(HandledConflict):
394
"""A provisionally-resolved path problem involving two paths.
395
This is intended to be a base class.
398
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
399
" %(file_id)r, %(conflict_file_id)r)"
401
def __init__(self, action, path, conflict_path, file_id=None,
402
conflict_file_id=None):
403
HandledConflict.__init__(self, action, path, file_id)
404
self.conflict_path = conflict_path
405
self.conflict_file_id = conflict_file_id
408
return HandledConflict._cmp_list(self) + [self.conflict_path,
409
self.conflict_file_id]
412
s = HandledConflict.as_stanza(self)
413
s.add('conflict_path', self.conflict_path)
414
if self.conflict_file_id is not None:
415
s.add('conflict_file_id', self.conflict_file_id)
420
class DuplicateID(HandledPathConflict):
421
"""Two files want the same file_id."""
423
typestring = 'duplicate id'
425
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
428
class DuplicateEntry(HandledPathConflict):
429
"""Two directory entries want to have the same name."""
431
typestring = 'duplicate'
433
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
436
class ParentLoop(HandledPathConflict):
437
"""An attempt to create an infinitely-looping directory structure.
438
This is rare, but can be produced like so:
447
typestring = 'parent loop'
449
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
452
class UnversionedParent(HandledConflict):
453
"""An attempt to version an file whose parent directory is not versioned.
454
Typically, the result of a merge where one tree unversioned the directory
455
and the other added a versioned file to it.
458
typestring = 'unversioned parent'
460
format = 'Conflict because %(path)s is not versioned, but has versioned'\
461
' children. %(action)s.'
464
class MissingParent(HandledConflict):
465
"""An attempt to add files to a directory that is not present.
466
Typically, the result of a merge where THIS deleted the directory and
467
the OTHER added a file to it.
468
See also: DeletingParent (same situation, reversed THIS and OTHER)
471
typestring = 'missing parent'
473
format = 'Conflict adding files to %(path)s. %(action)s.'
476
class DeletingParent(HandledConflict):
477
"""An attempt to add files to a directory that is not present.
478
Typically, the result of a merge where one OTHER deleted the directory and
479
the THIS added a file to it.
482
typestring = 'deleting parent'
484
format = "Conflict: can't delete %(path)s because it is not empty. "\
491
def register_types(*conflict_types):
492
"""Register a Conflict subclass for serialization purposes"""
494
for conflict_type in conflict_types:
495
ctype[conflict_type.typestring] = conflict_type
498
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
499
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,