49
38
def __init__(self, branch):
50
39
self.branch = branch
52
def get_tag_dict(self):
53
"""Return a dictionary mapping tags to revision ids.
55
raise NotImplementedError(self.get_tag_dict)
57
def get_reverse_tag_dict(self):
58
"""Return a dictionary mapping revision ids to list of tags.
60
raise NotImplementedError(self.get_reverse_tag_dict)
62
def merge_to(self, to_tags, overwrite=False, ignore_master=False):
63
"""Merge new tags from this tags container into another.
65
:param to_tags: Tags container to merge into
66
:param overwrite: Whether to overwrite existing, divergent, tags.
67
:param ignore_master: Do not modify the tags in the target's master
68
branch (if any). Default is false (so the master will be updated).
70
:return: Tuple with tag updates as dictionary and tag conflicts
72
raise NotImplementedError(self.merge_to)
74
def set_tag(self, tag_name, revision):
77
:param tag_name: Tag name
78
:param revision: Revision id
79
:raise GhostTagsNotSupported: if revision is not present in
82
raise NotImplementedError(self.set_tag)
84
def lookup_tag(self, tag_name):
87
:param tag_name: Tag to look up
88
:raise NoSuchTag: Raised when tag does not exist
89
:return: Matching revision id
91
raise NotImplementedError(self.lookup_tag)
93
def delete_tag(self, tag_name):
96
:param tag_name: Tag to delete
97
:raise NoSuchTag: Raised when tag does not exist
99
raise NotImplementedError(self.delete_tag)
101
def rename_revisions(self, rename_map):
102
"""Replace revision ids according to a rename map.
104
:param rename_map: Dictionary mapping old revision ids to
107
raise NotImplementedError(self.rename_revisions)
109
41
def has_tag(self, tag_name):
110
42
return self.get_tag_dict().has_key(tag_name)
257
189
:param to_tags: Branch to receive these tags
258
190
:param overwrite: Overwrite conflicting tags in the target branch
259
:param ignore_master: Do not modify the tags in the target's master
260
branch (if any). Default is false (so the master will be updated).
263
:returns: Tuple with tag_updates and tag_conflicts.
264
tag_updates is a dictionary with new tags, None is used for
266
tag_conflicts is a set of tags that conflicted, each of which is
192
:returns: A list of tags that conflicted, each of which is
267
193
(tagname, source_target, dest_target), or None if no copying was
270
operation = cleanup.OperationWithCleanups(self._merge_to_operation)
271
return operation.run(to_tags, overwrite, ignore_master)
273
def _merge_to_operation(self, operation, to_tags, overwrite, ignore_master):
274
add_cleanup = operation.add_cleanup
275
196
if self.branch == to_tags.branch:
277
198
if not self.branch.supports_tags():
278
199
# obviously nothing to copy
280
201
source_dict = self.get_tag_dict()
281
202
if not source_dict:
282
203
# no tags in the source, and we don't want to clobber anything
283
204
# that's in the destination
285
# We merge_to both master and child individually.
287
# It's possible for master and child to have differing sets of
288
# tags, in which case it's possible to have different sets of
289
# conflicts. We report the union of both conflict sets. In
290
# that case it's likely the child and master have accepted
291
# different tags from the source, which may be a surprising result, but
292
# the best we can do in the circumstances.
294
# Ideally we'd improve this API to report the different conflicts
295
# more clearly to the caller, but we don't want to break plugins
296
# such as bzr-builddeb that use this API.
297
add_cleanup(to_tags.branch.lock_write().unlock)
301
master = to_tags.branch.get_master_branch()
302
if master is not None:
303
add_cleanup(master.lock_write().unlock)
304
updates, conflicts = self._merge_to(to_tags, source_dict, overwrite)
305
if master is not None:
306
extra_updates, extra_conflicts = self._merge_to(master.tags,
307
source_dict, overwrite)
308
updates.update(extra_updates)
309
conflicts += extra_conflicts
310
# We use set() to remove any duplicate conflicts from the master
312
return updates, set(conflicts)
314
def _merge_to(self, to_tags, source_dict, overwrite):
315
dest_dict = to_tags.get_tag_dict()
316
result, updates, conflicts = self._reconcile_tags(source_dict,
317
dest_dict, overwrite)
318
if result != dest_dict:
319
to_tags._set_tag_dict(result)
320
return updates, conflicts
206
to_tags.branch.lock_write()
208
dest_dict = to_tags.get_tag_dict()
209
result, conflicts = self._reconcile_tags(source_dict, dest_dict,
211
if result != dest_dict:
212
to_tags._set_tag_dict(result)
214
to_tags.branch.unlock()
322
217
def rename_revisions(self, rename_map):
323
218
"""Rename revisions in this tags dictionary.
325
220
:param rename_map: Dictionary mapping old revids to new revids
327
222
reverse_tags = self.get_reverse_tag_dict()
333
228
def _reconcile_tags(self, source_dict, dest_dict, overwrite):
334
229
"""Do a two-way merge of two tag dictionaries.
336
* only in source => source value
337
* only in destination => destination value
338
* same definitions => that
339
* different definitions => if overwrite is False, keep destination
340
value and give a warning, otherwise use the source value
231
only in source => source value
232
only in destination => destination value
233
same definitions => that
234
different definitions => if overwrite is False, keep destination
235
value and give a warning, otherwise use the source value
342
:returns: (result_dict, updates,
237
:returns: (result_dict,
343
238
[(conflicting_tag, source_target, dest_target)])
347
241
result = dict(dest_dict) # copy
348
242
for name, target in source_dict.items():
349
if result.get(name) == target:
243
if name not in result or overwrite:
244
result[name] = target
245
elif result[name] == target:
351
elif name not in result or overwrite:
352
updates[name] = target
353
result[name] = target
355
248
conflicts.append((name, target, result[name]))
356
return result, updates, conflicts
359
def _merge_tags_if_possible(from_branch, to_branch, ignore_master=False):
360
# Try hard to support merge_to implementations that don't expect
361
# 'ignore_master' (new in bzr 2.3). First, if the flag isn't set then we
362
# can safely avoid passing ignore_master at all.
363
if not ignore_master:
364
from_branch.tags.merge_to(to_branch.tags)
366
# If the flag is set, try to pass it, but be ready to catch TypeError.
368
from_branch.tags.merge_to(to_branch.tags, ignore_master=ignore_master)
370
# Probably this implementation of 'merge_to' is from a plugin that
371
# doesn't expect the 'ignore_master' keyword argument (e.g. bzr-svn
372
# 1.0.4). There's a small risk that the TypeError is actually caused
373
# by a completely different problem (which is why we don't catch it for
374
# the ignore_master=False case), but even then there's probably no harm
375
# in calling a second time.
376
symbol_versioning.warn(
377
symbol_versioning.deprecated_in((2,3)) % (
378
"Tags.merge_to (of %r) that doesn't accept ignore_master kwarg"
379
% (from_branch.tags,),),
381
from_branch.tags.merge_to(to_branch.tags)
384
def sort_natural(branch, tags):
385
"""Sort tags, with numeric substrings as numbers.
387
:param branch: Branch
388
:param tags: List of tuples with tag name and revision id.
390
def natural_sort_key(tag):
391
return [f(s) for f,s in
392
zip(itertools.cycle((unicode.lower,int)),
393
re.split('([0-9]+)', tag[0]))]
394
tags.sort(key=natural_sort_key)
397
def sort_alpha(branch, tags):
398
"""Sort tags lexicographically, in place.
400
:param branch: Branch
401
:param tags: List of tuples with tag name and revision id.
406
def sort_time(branch, tags):
407
"""Sort tags by time inline.
409
:param branch: Branch
410
:param tags: List of tuples with tag name and revision id.
413
for tag, revid in tags:
415
revobj = branch.repository.get_revision(revid)
416
except errors.NoSuchRevision:
417
timestamp = sys.maxint # place them at the end
419
timestamp = revobj.timestamp
420
timestamps[revid] = timestamp
421
tags.sort(key=lambda x: timestamps[x[1]])
424
tag_sort_methods = Registry()
425
tag_sort_methods.register("natural", sort_natural,
426
'Sort numeric substrings as numbers. (default)')
427
tag_sort_methods.register("alpha", sort_alpha, 'Sort tags lexicographically.')
428
tag_sort_methods.register("time", sort_time, 'Sort tags chronologically.')
429
tag_sort_methods.default_key = "natural"
249
return result, conflicts
252
def _merge_tags_if_possible(from_branch, to_branch):
253
from_branch.tags.merge_to(to_branch.tags)