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
from __future__ import absolute_import
23
from ..lazy_import import lazy_import
24
lazy_import(globals(), """
35
from ..conflicts import (
36
Conflict as BaseConflict,
37
ConflictList as BaseConflictList,
39
from ..sixish import text_type
42
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
45
class Conflict(BaseConflict):
46
"""Base class for all types of conflict"""
48
# FIXME: cleanup should take care of that ? -- vila 091229
51
def __init__(self, path, file_id=None):
52
super(Conflict, self).__init__(path)
53
# the factory blindly transfers the Stanza values to __init__ and
54
# Stanza is purely a Unicode api.
55
if isinstance(file_id, text_type):
56
file_id = cache_utf8.encode(file_id)
57
self.file_id = file_id
60
s = rio.Stanza(type=self.typestring, path=self.path)
61
if self.file_id is not None:
62
# Stanza requires Unicode apis
63
s.add('file_id', self.file_id.decode('utf8'))
67
return [type(self), self.path, self.file_id]
69
def __cmp__(self, other):
70
if getattr(other, "_cmp_list", None) is None:
74
return (x > y) - (x < y)
77
return hash((type(self), self.path, self.file_id))
79
def __eq__(self, other):
80
return self.__cmp__(other) == 0
82
def __ne__(self, other):
83
return not self.__eq__(other)
85
def __unicode__(self):
86
return self.describe()
89
return self.describe()
92
return self.format % self.__dict__
95
rdict = dict(self.__dict__)
96
rdict['class'] = self.__class__.__name__
97
return self.rformat % rdict
100
def factory(type, **kwargs):
102
return ctype[type](**kwargs)
105
def sort_key(conflict):
106
if conflict.path is not None:
107
return conflict.path, conflict.typestring
108
elif getattr(conflict, "conflict_path", None) is not None:
109
return conflict.conflict_path, conflict.typestring
111
return None, conflict.typestring
113
def do(self, action, tree):
114
"""Apply the specified action to the conflict.
116
:param action: The method name to call.
118
:param tree: The tree passed as a parameter to the method.
120
meth = getattr(self, 'action_%s' % action, None)
122
raise NotImplementedError(self.__class__.__name__ + '.' + action)
125
def action_auto(self, tree):
126
raise NotImplementedError(self.action_auto)
128
def action_done(self, tree):
129
"""Mark the conflict as solved once it has been handled."""
130
# This method does nothing but simplifies the design of upper levels.
133
def action_take_this(self, tree):
134
raise NotImplementedError(self.action_take_this)
136
def action_take_other(self, tree):
137
raise NotImplementedError(self.action_take_other)
139
def _resolve_with_cleanups(self, tree, *args, **kwargs):
140
with tree.transform() as tt:
141
self._resolve(tt, *args, **kwargs)
144
class ConflictList(BaseConflictList):
147
def from_stanzas(stanzas):
148
"""Produce a new ConflictList from an iterable of stanzas"""
149
conflicts = ConflictList()
150
for stanza in stanzas:
151
conflicts.append(Conflict.factory(**stanza.as_dict()))
154
def to_stanzas(self):
155
"""Generator of stanzas"""
156
for conflict in self:
157
yield conflict.as_stanza()
159
def select_conflicts(self, tree, paths, ignore_misses=False,
161
"""Select the conflicts associated with paths in a tree.
163
File-ids are also used for this.
164
:return: a pair of ConflictLists: (not_selected, selected)
166
path_set = set(paths)
168
selected_paths = set()
169
new_conflicts = ConflictList()
170
selected_conflicts = ConflictList()
172
file_id = tree.path2id(path)
173
if file_id is not None:
176
for conflict in self:
178
for key in ('path', 'conflict_path'):
179
cpath = getattr(conflict, key, None)
182
if cpath in path_set:
184
selected_paths.add(cpath)
186
if osutils.is_inside_any(path_set, cpath):
188
selected_paths.add(cpath)
190
for key in ('file_id', 'conflict_file_id'):
191
cfile_id = getattr(conflict, key, None)
195
cpath = ids[cfile_id]
199
selected_paths.add(cpath)
201
selected_conflicts.append(conflict)
203
new_conflicts.append(conflict)
204
if ignore_misses is not True:
205
for path in [p for p in paths if p not in selected_paths]:
206
if not os.path.exists(tree.abspath(path)):
207
print("%s does not exist" % path)
209
print("%s is not conflicted" % path)
210
return new_conflicts, selected_conflicts
215
class PathConflict(Conflict):
216
"""A conflict was encountered merging file paths"""
218
typestring = 'path conflict'
220
format = 'Path conflict: %(path)s / %(conflict_path)s'
222
rformat = '%(class)s(%(path)r, %(conflict_path)r, %(file_id)r)'
224
def __init__(self, path, conflict_path=None, file_id=None):
225
Conflict.__init__(self, path, file_id)
226
self.conflict_path = conflict_path
229
s = Conflict.as_stanza(self)
230
if self.conflict_path is not None:
231
s.add('conflict_path', self.conflict_path)
234
def associated_filenames(self):
235
# No additional files have been generated here
238
def _resolve(self, tt, file_id, path, winner):
239
"""Resolve the conflict.
241
:param tt: The TreeTransform where the conflict is resolved.
242
:param file_id: The retained file id.
243
:param path: The retained path.
244
:param winner: 'this' or 'other' indicates which side is the winner.
246
path_to_create = None
248
if self.path == '<deleted>':
249
return # Nothing to do
250
if self.conflict_path == '<deleted>':
251
path_to_create = self.path
252
revid = tt._tree.get_parent_ids()[0]
253
elif winner == 'other':
254
if self.conflict_path == '<deleted>':
255
return # Nothing to do
256
if self.path == '<deleted>':
257
path_to_create = self.conflict_path
258
# FIXME: If there are more than two parents we may need to
259
# iterate. Taking the last parent is the safer bet in the mean
260
# time. -- vila 20100309
261
revid = tt._tree.get_parent_ids()[-1]
264
raise AssertionError('bad winner: %r' % (winner,))
265
if path_to_create is not None:
266
tid = tt.trans_id_tree_path(path_to_create)
267
tree = self._revision_tree(tt._tree, revid)
268
transform.create_from_tree(
269
tt, tid, tree, tree.id2path(file_id))
270
tt.version_file(tid, file_id=file_id)
272
tid = tt.trans_id_file_id(file_id)
273
# Adjust the path for the retained file id
274
parent_tid = tt.get_tree_parent(tid)
275
tt.adjust_path(osutils.basename(path), parent_tid, tid)
278
def _revision_tree(self, tree, revid):
279
return tree.branch.repository.revision_tree(revid)
281
def _infer_file_id(self, tree):
282
# Prior to bug #531967, file_id wasn't always set, there may still be
283
# conflict files in the wild so we need to cope with them
284
# Establish which path we should use to find back the file-id
286
for p in (self.path, self.conflict_path):
288
# special hard-coded path
291
possible_paths.append(p)
292
# Search the file-id in the parents with any path available
294
for revid in tree.get_parent_ids():
295
revtree = self._revision_tree(tree, revid)
296
for p in possible_paths:
297
file_id = revtree.path2id(p)
298
if file_id is not None:
299
return revtree, file_id
302
def action_take_this(self, tree):
303
if self.file_id is not None:
304
self._resolve_with_cleanups(tree, self.file_id, self.path,
307
# Prior to bug #531967 we need to find back the file_id and restore
308
# the content from there
309
revtree, file_id = self._infer_file_id(tree)
310
tree.revert([revtree.id2path(file_id)],
311
old_tree=revtree, backups=False)
313
def action_take_other(self, tree):
314
if self.file_id is not None:
315
self._resolve_with_cleanups(tree, self.file_id,
319
# Prior to bug #531967 we need to find back the file_id and restore
320
# the content from there
321
revtree, file_id = self._infer_file_id(tree)
322
tree.revert([revtree.id2path(file_id)],
323
old_tree=revtree, backups=False)
326
class ContentsConflict(PathConflict):
327
"""The files are of different types (or both binary), or not present"""
331
typestring = 'contents conflict'
333
format = 'Contents conflict in %(path)s'
335
def associated_filenames(self):
336
return [self.path + suffix for suffix in ('.BASE', '.OTHER')]
338
def _resolve(self, tt, suffix_to_remove):
339
"""Resolve the conflict.
341
:param tt: The TreeTransform where the conflict is resolved.
342
:param suffix_to_remove: Either 'THIS' or 'OTHER'
344
The resolution is symmetric: when taking THIS, OTHER is deleted and
345
item.THIS is renamed into item and vice-versa.
348
# Delete 'item.THIS' or 'item.OTHER' depending on
351
tt.trans_id_tree_path(self.path + '.' + suffix_to_remove))
352
except errors.NoSuchFile:
353
# There are valid cases where 'item.suffix_to_remove' either
354
# never existed or was already deleted (including the case
355
# where the user deleted it)
358
this_path = tt._tree.id2path(self.file_id)
359
except errors.NoSuchId:
360
# The file is not present anymore. This may happen if the user
361
# deleted the file either manually or when resolving a conflict on
362
# the parent. We may raise some exception to indicate that the
363
# conflict doesn't exist anymore and as such doesn't need to be
364
# resolved ? -- vila 20110615
367
this_tid = tt.trans_id_tree_path(this_path)
368
if this_tid is not None:
369
# Rename 'item.suffix_to_remove' (note that if
370
# 'item.suffix_to_remove' has been deleted, this is a no-op)
371
parent_tid = tt.get_tree_parent(this_tid)
372
tt.adjust_path(osutils.basename(self.path), parent_tid, this_tid)
375
def action_take_this(self, tree):
376
self._resolve_with_cleanups(tree, 'OTHER')
378
def action_take_other(self, tree):
379
self._resolve_with_cleanups(tree, 'THIS')
382
# TODO: There should be a base revid attribute to better inform the user about
383
# how the conflicts were generated.
384
class TextConflict(Conflict):
385
"""The merge algorithm could not resolve all differences encountered."""
389
typestring = 'text conflict'
391
format = 'Text conflict in %(path)s'
393
rformat = '%(class)s(%(path)r, %(file_id)r)'
395
_conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
397
def associated_filenames(self):
398
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
400
def _resolve(self, tt, winner_suffix):
401
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
403
:param tt: The TreeTransform where the conflict is resolved.
404
:param winner_suffix: Either 'THIS' or 'OTHER'
406
The resolution is symmetric, when taking THIS, item.THIS is renamed
407
into item and vice-versa. This takes one of the files as a whole
408
ignoring every difference that could have been merged cleanly.
410
# To avoid useless copies, we switch item and item.winner_suffix, only
411
# item will exist after the conflict has been resolved anyway.
412
item_tid = tt.trans_id_file_id(self.file_id)
413
item_parent_tid = tt.get_tree_parent(item_tid)
414
winner_path = self.path + '.' + winner_suffix
415
winner_tid = tt.trans_id_tree_path(winner_path)
416
winner_parent_tid = tt.get_tree_parent(winner_tid)
417
# Switch the paths to preserve the content
418
tt.adjust_path(osutils.basename(self.path),
419
winner_parent_tid, winner_tid)
420
tt.adjust_path(osutils.basename(winner_path),
421
item_parent_tid, item_tid)
422
# Associate the file_id to the right content
423
tt.unversion_file(item_tid)
424
tt.version_file(winner_tid, file_id=self.file_id)
427
def action_auto(self, tree):
428
# GZ 2012-07-27: Using NotImplementedError to signal that a conflict
429
# can't be auto resolved does not seem ideal.
431
kind = tree.kind(self.path)
432
except errors.NoSuchFile:
435
raise NotImplementedError("Conflict is not a file")
436
conflict_markers_in_line = self._conflict_re.search
437
# GZ 2012-07-27: What if not tree.has_id(self.file_id) due to removal?
438
with tree.get_file(self.path) as f:
440
if conflict_markers_in_line(line):
441
raise NotImplementedError("Conflict markers present")
443
def action_take_this(self, tree):
444
self._resolve_with_cleanups(tree, 'THIS')
446
def action_take_other(self, tree):
447
self._resolve_with_cleanups(tree, 'OTHER')
450
class HandledConflict(Conflict):
451
"""A path problem that has been provisionally resolved.
452
This is intended to be a base class.
455
rformat = "%(class)s(%(action)r, %(path)r, %(file_id)r)"
457
def __init__(self, action, path, file_id=None):
458
Conflict.__init__(self, path, file_id)
462
return Conflict._cmp_list(self) + [self.action]
465
s = Conflict.as_stanza(self)
466
s.add('action', self.action)
469
def associated_filenames(self):
470
# Nothing has been generated here
474
class HandledPathConflict(HandledConflict):
475
"""A provisionally-resolved path problem involving two paths.
476
This is intended to be a base class.
479
rformat = "%(class)s(%(action)r, %(path)r, %(conflict_path)r,"\
480
" %(file_id)r, %(conflict_file_id)r)"
482
def __init__(self, action, path, conflict_path, file_id=None,
483
conflict_file_id=None):
484
HandledConflict.__init__(self, action, path, file_id)
485
self.conflict_path = conflict_path
486
# the factory blindly transfers the Stanza values to __init__,
487
# so they can be unicode.
488
if isinstance(conflict_file_id, text_type):
489
conflict_file_id = cache_utf8.encode(conflict_file_id)
490
self.conflict_file_id = conflict_file_id
493
return HandledConflict._cmp_list(self) + [self.conflict_path,
494
self.conflict_file_id]
497
s = HandledConflict.as_stanza(self)
498
s.add('conflict_path', self.conflict_path)
499
if self.conflict_file_id is not None:
500
s.add('conflict_file_id', self.conflict_file_id.decode('utf8'))
505
class DuplicateID(HandledPathConflict):
506
"""Two files want the same file_id."""
508
typestring = 'duplicate id'
510
format = 'Conflict adding id to %(conflict_path)s. %(action)s %(path)s.'
513
class DuplicateEntry(HandledPathConflict):
514
"""Two directory entries want to have the same name."""
516
typestring = 'duplicate'
518
format = 'Conflict adding file %(conflict_path)s. %(action)s %(path)s.'
520
def action_take_this(self, tree):
521
tree.remove([self.conflict_path], force=True, keep_files=False)
522
tree.rename_one(self.path, self.conflict_path)
524
def action_take_other(self, tree):
525
tree.remove([self.path], force=True, keep_files=False)
528
class ParentLoop(HandledPathConflict):
529
"""An attempt to create an infinitely-looping directory structure.
530
This is rare, but can be produced like so:
539
typestring = 'parent loop'
541
format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
543
def action_take_this(self, tree):
544
# just acccept brz proposal
547
def action_take_other(self, tree):
548
with tree.transform() as tt:
549
p_tid = tt.trans_id_file_id(self.file_id)
550
parent_tid = tt.get_tree_parent(p_tid)
551
cp_tid = tt.trans_id_file_id(self.conflict_file_id)
552
cparent_tid = tt.get_tree_parent(cp_tid)
553
tt.adjust_path(osutils.basename(self.path), cparent_tid, cp_tid)
554
tt.adjust_path(osutils.basename(self.conflict_path),
559
class UnversionedParent(HandledConflict):
560
"""An attempt to version a file whose parent directory is not versioned.
561
Typically, the result of a merge where one tree unversioned the directory
562
and the other added a versioned file to it.
565
typestring = 'unversioned parent'
567
format = 'Conflict because %(path)s is not versioned, but has versioned'\
568
' children. %(action)s.'
570
# FIXME: We silently do nothing to make tests pass, but most probably the
571
# conflict shouldn't exist (the long story is that the conflict is
572
# generated with another one that can be resolved properly) -- vila 091224
573
def action_take_this(self, tree):
576
def action_take_other(self, tree):
580
class MissingParent(HandledConflict):
581
"""An attempt to add files to a directory that is not present.
582
Typically, the result of a merge where THIS deleted the directory and
583
the OTHER added a file to it.
584
See also: DeletingParent (same situation, THIS and OTHER reversed)
587
typestring = 'missing parent'
589
format = 'Conflict adding files to %(path)s. %(action)s.'
591
def action_take_this(self, tree):
592
tree.remove([self.path], force=True, keep_files=False)
594
def action_take_other(self, tree):
595
# just acccept brz proposal
599
class DeletingParent(HandledConflict):
600
"""An attempt to add files to a directory that is not present.
601
Typically, the result of a merge where one OTHER deleted the directory and
602
the THIS added a file to it.
605
typestring = 'deleting parent'
607
format = "Conflict: can't delete %(path)s because it is not empty. "\
610
# FIXME: It's a bit strange that the default action is not coherent with
611
# MissingParent from the *user* pov.
613
def action_take_this(self, tree):
614
# just acccept brz proposal
617
def action_take_other(self, tree):
618
tree.remove([self.path], force=True, keep_files=False)
621
class NonDirectoryParent(HandledConflict):
622
"""An attempt to add files to a directory that is not a directory or
623
an attempt to change the kind of a directory with files.
626
typestring = 'non-directory parent'
628
format = "Conflict: %(path)s is not a directory, but has files in it."\
631
# FIXME: .OTHER should be used instead of .new when the conflict is created
633
def action_take_this(self, tree):
634
# FIXME: we should preserve that path when the conflict is generated !
635
if self.path.endswith('.new'):
636
conflict_path = self.path[:-(len('.new'))]
637
tree.remove([self.path], force=True, keep_files=False)
638
tree.add(conflict_path)
640
raise NotImplementedError(self.action_take_this)
642
def action_take_other(self, tree):
643
# FIXME: we should preserve that path when the conflict is generated !
644
if self.path.endswith('.new'):
645
conflict_path = self.path[:-(len('.new'))]
646
tree.remove([conflict_path], force=True, keep_files=False)
647
tree.rename_one(self.path, conflict_path)
649
raise NotImplementedError(self.action_take_other)
655
def register_types(*conflict_types):
656
"""Register a Conflict subclass for serialization purposes"""
658
for conflict_type in conflict_types:
659
ctype[conflict_type.typestring] = conflict_type
662
register_types(ContentsConflict, TextConflict, PathConflict, DuplicateID,
663
DuplicateEntry, ParentLoop, UnversionedParent, MissingParent,
664
DeletingParent, NonDirectoryParent)