1
# Copyright (C) 2005 by Aaron Bentley
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
26
from bzrlib.commands import register_command
27
from bzrlib.errors import BzrCommandError, NotConflicted, UnsupportedOperation
28
from bzrlib.option import Option
29
from bzrlib.osutils import rename
30
from bzrlib.rio import Stanza
33
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
36
class cmd_conflicts(bzrlib.commands.Command):
37
"""List files with conflicts.
39
Merge will do its best to combine the changes in two branches, but there
40
are some kinds of problems only a human can fix. When it encounters those,
41
it will mark a conflict. A conflict means that you need to fix something,
42
before you should commit.
44
Use bzr resolve when you have fixed a problem.
46
(conflicts are determined by the presence of .BASE .TREE, and .OTHER
52
from bzrlib.workingtree import WorkingTree
53
wt = WorkingTree.open_containing(u'.')[0]
54
for conflict in wt.conflicts():
57
class cmd_resolve(bzrlib.commands.Command):
58
"""Mark a conflict as resolved.
60
Merge will do its best to combine the changes in two branches, but there
61
are some kinds of problems only a human can fix. When it encounters those,
62
it will mark a conflict. A conflict means that you need to fix something,
63
before you should commit.
65
Once you have fixed a problem, use "bzr resolve FILE.." to mark
66
individual files as fixed, or "bzr resolve --all" to mark all conflicts as
69
See also bzr conflicts.
71
aliases = ['resolved']
72
takes_args = ['file*']
73
takes_options = [Option('all', help='Resolve all conflicts in this tree')]
74
def run(self, file_list=None, all=False):
75
from bzrlib.workingtree import WorkingTree
78
raise BzrCommandError(
79
"command 'resolve' needs one or more FILE, or --all")
82
raise BzrCommandError(
83
"If --all is specified, no FILE may be provided")
84
tree = WorkingTree.open_containing(u'.')[0]
85
resolve(tree, file_list)
88
def resolve(tree, paths=None, ignore_misses=False):
91
tree_conflicts = tree.conflicts()
94
selected_conflicts = tree_conflicts
96
new_conflicts, selected_conflicts = \
97
select_conflicts(tree, paths, tree_conflicts, ignore_misses)
99
tree.set_conflicts(ConflictList(new_conflicts))
100
except UnsupportedOperation:
102
selected_conflicts.remove_files(tree)
107
def select_conflicts(tree, paths, tree_conflicts, ignore_misses=False):
108
path_set = set(paths)
110
selected_paths = set()
112
selected_conflicts = ConflictList()
114
file_id = tree.path2id(path)
115
if file_id is not None:
118
for conflict, stanza in zip(tree_conflicts, tree_conflicts.to_stanzas()):
120
for key in ('path', 'conflict_path'):
125
if cpath in path_set:
127
selected_paths.add(cpath)
128
for key in ('file_id', 'conflict_file_id'):
130
cfile_id = stanza[key]
134
cpath = ids[cfile_id]
138
selected_paths.add(cpath)
140
selected_conflicts.append(conflict)
142
new_conflicts.append(conflict)
143
if ignore_misses is not True:
144
for path in [p for p in paths if p not in selected_paths]:
145
if not os.path.exists(tree.abspath(path)):
146
print "%s does not exist" % path
148
print "%s is not conflicted" % path
149
return ConflictList(new_conflicts), ConflictList(selected_conflicts)
153
def restore(filename):
155
Restore a conflicted file to the state it was in before merging.
156
Only text restoration supported at present.
160
rename(filename + ".THIS", filename)
163
if e.errno != errno.ENOENT:
166
os.unlink(filename + ".BASE")
169
if e.errno != errno.ENOENT:
172
os.unlink(filename + ".OTHER")
175
if e.errno != errno.ENOENT:
178
raise NotConflicted(filename)
181
class ConflictList(object):
183
Can be instantiated from stanzas or from Conflict subclasses.
186
def __init__(self, conflicts=None):
187
object.__init__(self)
188
if conflicts is None:
191
self.__list = conflicts
194
return len(self.__list)
197
return iter(self.__list)
199
def __getitem__(self, key):
200
return self.__list[key]
202
def append(self, conflict):
203
return self.__list.append(conflict)
205
def __eq__(self, other_list):
206
return list(self) == list(other_list)
208
def __ne__(self, other_list):
209
return not (self == other_list)
212
return "ConflictList(%r)" % self.__list
215
def from_stanzas(stanzas):
216
"""Produce a new ConflictList from an iterable of stanzas"""
217
conflicts = ConflictList()
218
for stanza in stanzas:
219
conflicts.append(Conflict.factory(**stanza.as_dict()))
222
def to_stanzas(self):
223
"""Generator of stanzas"""
224
for conflict in self:
225
yield conflict.as_stanza()
227
def to_strings(self):
228
"""Generate strings for the provided conflicts"""
229
for conflict in self:
233
def remove_files(self, tree):
234
for conflict in self:
235
if not conflict.has_files:
237
for suffix in CONFLICT_SUFFIXES:
239
os.unlink(tree.abspath(conflict.path+suffix))
241
if e.errno != errno.ENOENT:
244
class Conflict(object):
245
"""Base class for all types of conflict"""
249
def __init__(self, path, file_id=None):
251
self.file_id = file_id
254
s = Stanza(type=self.typestring, path=self.path)
255
if self.file_id is not None:
256
s.add('file_id', self.file_id)
260
return [type(self), self.path, self.file_id]
262
def __cmp__(self, other):
263
if getattr(other, "_cmp_list", None) is None:
265
return cmp(self._cmp_list(), other._cmp_list())
267
def __eq__(self, other):
268
return self.__cmp__(other) == 0
270
def __ne__(self, other):
271
return not self.__eq__(other)
274
return self.format % self.__dict__
277
rdict = dict(self.__dict__)
278
rdict['class'] = self.__class__.__name__
279
return self.rformat % rdict
282
def factory(type, **kwargs):
284
return ctype[type](**kwargs)
287
class PathConflict(Conflict):
288
"""A conflict was encountered merging file paths"""
290
typestring = 'path conflict'
292
format = 'Path conflict: %(path)s / %(conflict_path)s'
294
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
295
def __init__(self, path, conflict_path=None, file_id=None):
296
Conflict.__init__(self, path, file_id)
297
self.conflict_path = conflict_path
300
s = Conflict.as_stanza(self)
301
if self.conflict_path is not None:
302
s.add('conflict_path', self.conflict_path)
306
class ContentsConflict(PathConflict):
307
"""The files are of different types, or not present"""
311
typestring = 'contents conflict'
313
format = 'Contents conflict in %(path)s'
316
class TextConflict(PathConflict):
317
"""The merge algorithm could not resolve all differences encountered."""
321
typestring = 'text conflict'
323
format = 'Text conflict in %(path)s'
326
class HandledConflict(Conflict):
327
"""A path problem that has been provisionally resolved.
328
This is intended to be a base class.
331
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
333
def __init__(self, action, path, file_id=None):
334
Conflict.__init__(self, path, file_id)
338
return Conflict._cmp_list(self) + [self.action]
341
s = Conflict.as_stanza(self)
342
s.add('action', self.action)
346
class HandledPathConflict(HandledConflict):
347
"""A provisionally-resolved path problem involving two paths.
348
This is intended to be a base class.
351
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
352
" %(file_id)r, %(conflict_file_id)r)"
354
def __init__(self, action, path, conflict_path, file_id=None,
355
conflict_file_id=None):
356
HandledConflict.__init__(self, action, path, file_id)
357
self.conflict_path = conflict_path
358
self.conflict_file_id = conflict_file_id
361
return HandledConflict._cmp_list(self) + [self.conflict_path,
362
self.conflict_file_id]
365
s = HandledConflict.as_stanza(self)
366
s.add('conflict_path', self.conflict_path)
367
if self.conflict_file_id is not None:
368
s.add('conflict_file_id', self.conflict_file_id)
373
class DuplicateID(HandledPathConflict):
374
"""Two files want the same file_id."""
376
typestring = 'duplicate id'
378
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
381
class DuplicateEntry(HandledPathConflict):
382
"""Two directory entries want to have the same name."""
384
typestring = 'duplicate'
386
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
389
class ParentLoop(HandledPathConflict):
390
"""An attempt to create an infinitely-looping directory structure.
391
This is rare, but can be produced like so:
400
typestring = 'parent loop'
402
format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
405
class UnversionedParent(HandledConflict):
406
"""An attempt to version an file whose parent directory is not versioned.
407
Typically, the result of a merge where one tree unversioned the directory
408
and the other added a versioned file to it.
411
typestring = 'unversioned parent'
413
format = 'Conflict adding versioned files to %(path)s. %(action)s.'
416
class MissingParent(HandledConflict):
417
"""An attempt to add files to a directory that is not present.
418
Typically, the result of a merge where one tree deleted the directory and
419
the other added a file to it.
422
typestring = 'missing parent'
424
format = 'Conflict adding files to %(path)s. %(action)s.'
431
def register_types(*conflict_types):
432
"""Register a Conflict subclass for serialization purposes"""
434
for conflict_type in conflict_types:
435
ctype[conflict_type.typestring] = conflict_type
438
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
439
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,)