25
25
from collections import defaultdict
28
31
# NOTE: I was going to call this tags.py, but vim seems to think all files
29
32
# called tags* are ctags files... mbp 20070220.
34
from .inter import InterObject
31
35
from .registry import Registry
32
from .lazy_import import lazy_import
33
lazy_import(globals(), """
49
def _reconcile_tags(source_dict, dest_dict, overwrite):
42
def _reconcile_tags(source_dict, dest_dict, overwrite, selector):
50
43
"""Do a two-way merge of two tag dictionaries.
52
45
* only in source => source value
83
78
raise NotImplementedError(self.get_tag_dict)
85
80
def get_reverse_tag_dict(self):
86
"""Return a dictionary mapping revision ids to list of tags.
88
raise NotImplementedError(self.get_reverse_tag_dict)
90
def merge_to(self, to_tags, overwrite=False, ignore_master=False):
91
"""Merge new tags from this tags container into another.
93
:param to_tags: Tags container to merge into
94
:param overwrite: Whether to overwrite existing, divergent, tags.
81
"""Returns a dict with revisions as keys
82
and a list of tags for that revision as value"""
83
d = self.get_tag_dict()
84
rev = defaultdict(set)
89
def merge_to(self, to_tags, overwrite=False, ignore_master=False, selector=None):
90
"""Copy tags between repositories if necessary and possible.
92
This method has common command-line behaviour about handling
95
All new definitions are copied across, except that tags that already
96
exist keep their existing definitions.
98
:param to_tags: Branch to receive these tags
99
:param overwrite: Overwrite conflicting tags in the target branch
95
100
:param ignore_master: Do not modify the tags in the target's master
96
101
branch (if any). Default is false (so the master will be updated).
98
:return: Tuple with tag updates as dictionary and tag conflicts
103
:returns: Tuple with tag_updates and tag_conflicts.
104
tag_updates is a dictionary with new tags, None is used for
106
tag_conflicts is a set of tags that conflicted, each of which is
107
(tagname, source_target, dest_target), or None if no copying was
100
raise NotImplementedError(self.merge_to)
110
intertags = InterTags.get(self, to_tags)
111
return intertags.merge(
112
overwrite=overwrite, ignore_master=ignore_master,
102
115
def set_tag(self, tag_name, revision):
127
140
raise NotImplementedError(self.delete_tag)
129
142
def rename_revisions(self, rename_map):
130
"""Replace revision ids according to a rename map.
143
"""Rename revisions in this tags dictionary.
132
:param rename_map: Dictionary mapping old revision ids to
145
:param rename_map: Dictionary mapping old revids to new revids
135
raise NotImplementedError(self.rename_revisions)
147
reverse_tags = self.get_reverse_tag_dict()
148
for revid, names in reverse_tags.items():
149
if revid in rename_map:
151
self.set_tag(name, rename_map[revid])
137
153
def has_tag(self, tag_name):
138
154
return tag_name in self.get_tag_dict()
141
class DisabledTags(_Tags):
157
class DisabledTags(Tags):
142
158
"""Tag storage that refuses to store anything.
144
160
This is used by older formats that can't store tags.
169
class BasicTags(_Tags):
170
"""Tag storage in an unversioned branch control file.
185
class InterTags(InterObject):
186
"""Operations between sets of tags.
173
def set_tag(self, tag_name, tag_target):
174
"""Add a tag definition to the branch.
176
Behaviour if the tag is already present is not defined (yet).
178
# all done with a write lock held, so this looks atomic
179
with self.branch.lock_write():
180
master = self.branch.get_master_branch()
181
if master is not None:
182
master.tags.set_tag(tag_name, tag_target)
183
td = self.get_tag_dict()
184
td[tag_name] = tag_target
185
self._set_tag_dict(td)
187
def lookup_tag(self, tag_name):
188
"""Return the referent string of a tag"""
189
td = self.get_tag_dict()
193
raise errors.NoSuchTag(tag_name)
195
def get_tag_dict(self):
196
with self.branch.lock_read():
198
tag_content = self.branch._get_tags_bytes()
199
except errors.NoSuchFile:
200
# ugly, but only abentley should see this :)
201
trace.warning('No branch/tags file in %s. '
202
'This branch was probably created by bzr 0.15pre. '
203
'Create an empty file to silence this message.'
206
return self._deserialize_tag_dict(tag_content)
208
def get_reverse_tag_dict(self):
209
"""Returns a dict with revisions as keys
210
and a list of tags for that revision as value"""
211
d = self.get_tag_dict()
212
rev = defaultdict(set)
217
def delete_tag(self, tag_name):
218
"""Delete a tag definition.
220
with self.branch.lock_write():
221
d = self.get_tag_dict()
225
raise errors.NoSuchTag(tag_name)
226
master = self.branch.get_master_branch()
227
if master is not None:
229
master.tags.delete_tag(tag_name)
230
except errors.NoSuchTag:
232
self._set_tag_dict(d)
234
def _set_tag_dict(self, new_dict):
235
"""Replace all tag definitions
237
WARNING: Calling this on an unlocked branch will lock it, and will
238
replace the tags without warning on conflicts.
240
:param new_dict: Dictionary from tag name to target.
242
return self.branch._set_tags_bytes(self._serialize_tag_dict(new_dict))
244
def _serialize_tag_dict(self, tag_dict):
245
td = dict((k.encode('utf-8'), v)
246
for k, v in tag_dict.items())
247
return bencode.bencode(td)
249
def _deserialize_tag_dict(self, tag_content):
250
"""Convert the tag file into a dictionary of tags"""
251
# was a special case to make initialization easy, an empty definition
252
# is an empty dictionary
253
if tag_content == b'':
257
for k, v in bencode.bdecode(tag_content).items():
258
r[k.decode('utf-8')] = v
260
except ValueError as e:
261
raise ValueError("failed to deserialize tag dictionary %r: %s"
264
def merge_to(self, to_tags, overwrite=False, ignore_master=False):
190
"""The available optimised InterTags types."""
193
def is_compatible(klass, source, target):
194
# This is the default implementation
197
def merge(self, overwrite=False, ignore_master=False, selector=None):
265
198
"""Copy tags between repositories if necessary and possible.
267
200
This method has common command-line behaviour about handling
286
221
with contextlib.ExitStack() as stack:
287
if self.branch == to_tags.branch:
222
if self.source.branch == self.target.branch:
289
if not self.branch.supports_tags():
224
if not self.source.branch.supports_tags():
290
225
# obviously nothing to copy
292
source_dict = self.get_tag_dict()
227
source_dict = self.source.get_tag_dict()
293
228
if not source_dict:
294
229
# no tags in the source, and we don't want to clobber anything
295
230
# that's in the destination
306
241
# Ideally we'd improve this API to report the different conflicts
307
242
# more clearly to the caller, but we don't want to break plugins
308
243
# such as bzr-builddeb that use this API.
309
stack.enter_context(to_tags.branch.lock_write())
244
stack.enter_context(self.target.branch.lock_write())
310
245
if ignore_master:
313
master = to_tags.branch.get_master_branch()
248
master = self.target.branch.get_master_branch()
314
249
if master is not None:
315
250
stack.enter_context(master.lock_write())
316
updates, conflicts = self._merge_to(to_tags, source_dict, overwrite)
251
updates, conflicts = self._merge_to(
252
self.target, source_dict, overwrite, selector=selector)
317
253
if master is not None:
318
extra_updates, extra_conflicts = self._merge_to(master.tags,
319
source_dict, overwrite)
254
extra_updates, extra_conflicts = self._merge_to(
255
master.tags, source_dict, overwrite, selector=selector)
320
256
updates.update(extra_updates)
321
257
conflicts += extra_conflicts
322
258
# We use set() to remove any duplicate conflicts from the master
324
260
return updates, set(conflicts)
326
def _merge_to(self, to_tags, source_dict, overwrite):
263
def _merge_to(cls, to_tags, source_dict, overwrite, selector):
327
264
dest_dict = to_tags.get_tag_dict()
328
265
result, updates, conflicts = _reconcile_tags(
329
source_dict, dest_dict, overwrite)
266
source_dict, dest_dict, overwrite, selector)
330
267
if result != dest_dict:
331
268
to_tags._set_tag_dict(result)
332
269
return updates, conflicts
334
def rename_revisions(self, rename_map):
335
"""Rename revisions in this tags dictionary.
337
:param rename_map: Dictionary mapping old revids to new revids
339
reverse_tags = self.get_reverse_tag_dict()
340
for revid, names in reverse_tags.items():
341
if revid in rename_map:
343
self.set_tag(name, rename_map[revid])
346
class MemoryTags(_Tags):
272
class MemoryTags(Tags):
348
274
def __init__(self, tag_dict):
349
275
self._tag_dict = tag_dict
385
302
def _set_tag_dict(self, result):
386
303
self._tag_dict = dict(result.items())
388
def merge_to(self, to_tags, overwrite=False, ignore_master=False):
305
def merge_to(self, to_tags, overwrite=False, ignore_master=False, selector=None):
389
306
source_dict = self.get_tag_dict()
390
307
dest_dict = to_tags.get_tag_dict()
391
308
result, updates, conflicts = _reconcile_tags(
392
source_dict, dest_dict, overwrite)
309
source_dict, dest_dict, overwrite, selector)
393
310
if result != dest_dict:
394
311
to_tags._set_tag_dict(result)
395
312
return updates, conflicts