1
# Copyright (C) 2005, 2006, 2007, 2009, 2010, 2011 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# TODO: 'brz resolve' should accept a directory name and work from that
24
from .lazy_import import lazy_import
25
lazy_import(globals(), """
30
from breezy.i18n import gettext, ngettext
43
class cmd_conflicts(commands.Command):
44
__doc__ = """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 can 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 brz resolve when you have fixed a problem.
60
help='List paths of files with text conflicts.'),
62
_see_also = ['resolve', 'conflict-types']
64
def run(self, text=False, directory=u'.'):
65
wt = workingtree.WorkingTree.open_containing(directory)[0]
66
for conflict in wt.conflicts():
68
if conflict.typestring != 'text conflict':
70
self.outf.write(conflict.path + '\n')
72
self.outf.write(str(conflict) + '\n')
75
resolve_action_registry = registry.Registry()
78
resolve_action_registry.register(
79
'auto', 'auto', 'Detect whether conflict has been resolved by user.')
80
resolve_action_registry.register(
81
'done', 'done', 'Marks the conflict as resolved.')
82
resolve_action_registry.register(
83
'take-this', 'take_this',
84
'Resolve the conflict preserving the version in the working tree.')
85
resolve_action_registry.register(
86
'take-other', 'take_other',
87
'Resolve the conflict taking the merged version into account.')
88
resolve_action_registry.default_key = 'done'
91
class ResolveActionOption(option.RegistryOption):
94
super(ResolveActionOption, self).__init__(
95
'action', 'How to resolve the conflict.',
97
registry=resolve_action_registry)
100
class cmd_resolve(commands.Command):
101
__doc__ = """Mark a conflict as resolved.
103
Merge will do its best to combine the changes in two branches, but there
104
are some kinds of problems only a human can fix. When it encounters those,
105
it will mark a conflict. A conflict means that you need to fix something,
106
before you can commit.
108
Once you have fixed a problem, use "brz resolve" to automatically mark
109
text conflicts as fixed, "brz resolve FILE" to mark a specific conflict as
110
resolved, or "brz resolve --all" to mark all conflicts as resolved.
112
aliases = ['resolved']
113
takes_args = ['file*']
116
option.Option('all', help='Resolve all conflicts in this tree.'),
117
ResolveActionOption(),
119
_see_also = ['conflicts']
121
def run(self, file_list=None, all=False, action=None, directory=None):
124
raise errors.CommandError(gettext("If --all is specified,"
125
" no FILE may be provided"))
126
if directory is None:
128
tree = workingtree.WorkingTree.open_containing(directory)[0]
132
tree, file_list = workingtree.WorkingTree.open_containing_paths(
133
file_list, directory)
135
if file_list is None:
139
before, after = resolve(tree, file_list, action=action)
140
# GZ 2012-07-27: Should unify UI below now that auto is less magical.
141
if action == 'auto' and file_list is None:
144
ngettext('%d conflict auto-resolved.',
145
'%d conflicts auto-resolved.', before - after),
147
trace.note(gettext('Remaining conflicts:'))
148
for conflict in tree.conflicts():
149
trace.note(str(conflict))
152
trace.note(gettext('All conflicts resolved.'))
155
trace.note(ngettext('{0} conflict resolved, {1} remaining',
156
'{0} conflicts resolved, {1} remaining',
157
before - after).format(before - after, after))
160
def resolve(tree, paths=None, ignore_misses=False, recursive=False,
162
"""Resolve some or all of the conflicts in a working tree.
164
:param paths: If None, resolve all conflicts. Otherwise, select only
166
:param recursive: If True, then elements of paths which are directories
167
have all their children resolved, etc. When invoked as part of
168
recursive commands like revert, this should be True. For commands
169
or applications wishing finer-grained control, like the resolve
170
command, this should be False.
171
:param ignore_misses: If False, warnings will be printed if the supplied
172
paths do not have conflicts.
173
:param action: How the conflict should be resolved,
175
nb_conflicts_after = None
176
with tree.lock_tree_write():
177
tree_conflicts = tree.conflicts()
178
nb_conflicts_before = len(tree_conflicts)
181
to_process = tree_conflicts
183
new_conflicts, to_process = tree_conflicts.select_conflicts(
184
tree, paths, ignore_misses, recursive)
185
for conflict in to_process:
187
conflict.do(action, tree)
188
conflict.cleanup(tree)
189
except NotImplementedError:
190
new_conflicts.append(conflict)
192
nb_conflicts_after = len(new_conflicts)
193
tree.set_conflicts(new_conflicts)
194
except errors.UnsupportedOperation:
196
if nb_conflicts_after is None:
197
nb_conflicts_after = nb_conflicts_before
198
return nb_conflicts_before, nb_conflicts_after
201
def restore(filename):
202
"""Restore a conflicted file to the state it was in before merging.
204
Only text restoration is supported at present.
208
osutils.rename(filename + ".THIS", filename)
211
if e.errno != errno.ENOENT:
214
os.unlink(filename + ".BASE")
217
if e.errno != errno.ENOENT:
220
os.unlink(filename + ".OTHER")
223
if e.errno != errno.ENOENT:
226
raise errors.NotConflicted(filename)
229
class ConflictList(object):
230
"""List of conflicts.
232
Typically obtained from WorkingTree.conflicts()
235
def __init__(self, conflicts=None):
236
object.__init__(self)
237
if conflicts is None:
240
self.__list = conflicts
243
return len(self.__list) == 0
246
return len(self.__list)
249
return iter(self.__list)
251
def __getitem__(self, key):
252
return self.__list[key]
254
def append(self, conflict):
255
return self.__list.append(conflict)
257
def __eq__(self, other_list):
258
return list(self) == list(other_list)
260
def __ne__(self, other_list):
261
return not (self == other_list)
264
return "ConflictList(%r)" % self.__list
266
def to_strings(self):
267
"""Generate strings for the provided conflicts"""
268
for conflict in self:
271
def remove_files(self, tree):
272
"""Remove the THIS, BASE and OTHER files for listed conflicts"""
273
for conflict in self:
274
if not conflict.has_files:
276
conflict.cleanup(tree)
278
def select_conflicts(self, tree, paths, ignore_misses=False,
280
"""Select the conflicts associated with paths in a tree.
282
:return: a pair of ConflictLists: (not_selected, selected)
284
path_set = set(paths)
285
selected_paths = set()
286
new_conflicts = ConflictList()
287
selected_conflicts = ConflictList()
289
for conflict in self:
291
if conflict.path in path_set:
293
selected_paths.add(conflict.path)
295
if osutils.is_inside_any(path_set, conflict.path):
297
selected_paths.add(conflict.path)
300
selected_conflicts.append(conflict)
302
new_conflicts.append(conflict)
303
if ignore_misses is not True:
304
for path in [p for p in paths if p not in selected_paths]:
305
if not os.path.exists(tree.abspath(path)):
306
print("%s does not exist" % path)
308
print("%s is not conflicted" % path)
309
return new_conflicts, selected_conflicts
312
class Conflict(object):
313
"""Base class for conflicts."""
317
def __init__(self, path):
320
def associated_filenames(self):
321
"""The names of the files generated to help resolve the conflict."""
322
raise NotImplementedError(self.associated_filenames)
324
def cleanup(self, tree):
325
for fname in self.associated_filenames():
327
osutils.delete_any(tree.abspath(fname))
329
if e.errno != errno.ENOENT:
332
def do(self, action, tree):
333
"""Apply the specified action to the conflict.
335
:param action: The method name to call.
337
:param tree: The tree passed as a parameter to the method.
339
raise NotImplementedError(self.do)
342
"""Return a string description of this conflict."""
343
raise NotImplementedError(self.describe)