~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tag.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007-2011 Canonical Ltd
 
1
# Copyright (C) 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""Tag strategies.
18
18
 
19
 
These are contained within a branch and normally constructed
20
 
when the branch is opened.  Clients should typically do
 
19
These are contained within a branch and normally constructed 
 
20
when the branch is opened.  Clients should typically do 
21
21
 
22
22
  Branch.tags.add('name', 'value')
23
23
"""
24
24
 
25
 
from __future__ import absolute_import
26
 
 
27
25
# NOTE: I was going to call this tags.py, but vim seems to think all files
28
26
# called tags* are ctags files... mbp 20070220.
29
27
 
30
 
from bzrlib.registry import Registry
31
 
from bzrlib.lazy_import import lazy_import
32
 
lazy_import(globals(), """
33
 
import itertools
34
 
import re
35
 
import sys
 
28
 
 
29
from warnings import warn
36
30
 
37
31
from bzrlib import (
38
 
    bencode,
39
 
    cleanup,
40
32
    errors,
41
 
    symbol_versioning,
42
33
    trace,
43
34
    )
44
 
""")
 
35
from bzrlib.util import bencode
45
36
 
46
37
 
47
38
class _Tags(object):
49
40
    def __init__(self, branch):
50
41
        self.branch = branch
51
42
 
52
 
    def get_tag_dict(self):
53
 
        """Return a dictionary mapping tags to revision ids.
54
 
        """
55
 
        raise NotImplementedError(self.get_tag_dict)
56
 
 
57
 
    def get_reverse_tag_dict(self):
58
 
        """Return a dictionary mapping revision ids to list of tags.
59
 
        """
60
 
        raise NotImplementedError(self.get_reverse_tag_dict)
61
 
 
62
 
    def merge_to(self, to_tags, overwrite=False, ignore_master=False):
63
 
        """Merge new tags from this tags container into another.
64
 
 
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).
69
 
            New in bzr 2.3.
70
 
        :return: Tuple with tag updates as dictionary and tag conflicts
71
 
        """
72
 
        raise NotImplementedError(self.merge_to)
73
 
 
74
 
    def set_tag(self, tag_name, revision):
75
 
        """Set a tag.
76
 
 
77
 
        :param tag_name: Tag name
78
 
        :param revision: Revision id
79
 
        :raise GhostTagsNotSupported: if revision is not present in
80
 
            the branch repository
81
 
        """
82
 
        raise NotImplementedError(self.set_tag)
83
 
 
84
 
    def lookup_tag(self, tag_name):
85
 
        """Look up a tag.
86
 
 
87
 
        :param tag_name: Tag to look up
88
 
        :raise NoSuchTag: Raised when tag does not exist
89
 
        :return: Matching revision id
90
 
        """
91
 
        raise NotImplementedError(self.lookup_tag)
92
 
 
93
 
    def delete_tag(self, tag_name):
94
 
        """Delete a tag.
95
 
 
96
 
        :param tag_name: Tag to delete
97
 
        :raise NoSuchTag: Raised when tag does not exist
98
 
        """
99
 
        raise NotImplementedError(self.delete_tag)
100
 
 
101
 
    def rename_revisions(self, rename_map):
102
 
        """Replace revision ids according to a rename map.
103
 
 
104
 
        :param rename_map: Dictionary mapping old revision ids to
105
 
            new revision ids.
106
 
        """
107
 
        raise NotImplementedError(self.rename_revisions)
108
 
 
109
43
    def has_tag(self, tag_name):
110
44
        return self.get_tag_dict().has_key(tag_name)
111
45
 
119
53
    def _not_supported(self, *a, **k):
120
54
        raise errors.TagsNotSupported(self.branch)
121
55
 
 
56
    def supports_tags(self):
 
57
        return False
 
58
 
122
59
    set_tag = _not_supported
123
60
    get_tag_dict = _not_supported
124
61
    _set_tag_dict = _not_supported
125
62
    lookup_tag = _not_supported
126
63
    delete_tag = _not_supported
127
64
 
128
 
    def merge_to(self, to_tags, overwrite=False, ignore_master=False):
 
65
    def merge_to(self, to_tags, overwrite=False):
129
66
        # we never have anything to copy
130
 
        return {}, []
131
 
 
132
 
    def rename_revisions(self, rename_map):
133
 
        # No tags, so nothing to rename
134
67
        pass
135
68
 
136
69
    def get_reverse_tag_dict(self):
142
75
    """Tag storage in an unversioned branch control file.
143
76
    """
144
77
 
 
78
    def supports_tags(self):
 
79
        return True
 
80
 
145
81
    def set_tag(self, tag_name, tag_target):
146
82
        """Add a tag definition to the branch.
147
83
 
171
107
        self.branch.lock_read()
172
108
        try:
173
109
            try:
174
 
                tag_content = self.branch._get_tags_bytes()
 
110
                tag_content = self.branch._transport.get_bytes('tags')
175
111
            except errors.NoSuchFile, e:
176
112
                # ugly, but only abentley should see this :)
177
113
                trace.warning('No branch/tags file in %s.  '
218
154
    def _set_tag_dict(self, new_dict):
219
155
        """Replace all tag definitions
220
156
 
221
 
        WARNING: Calling this on an unlocked branch will lock it, and will
222
 
        replace the tags without warning on conflicts.
223
 
 
224
157
        :param new_dict: Dictionary from tag name to target.
225
158
        """
226
 
        return self.branch._set_tags_bytes(self._serialize_tag_dict(new_dict))
 
159
        self.branch.lock_write()
 
160
        try:
 
161
            self.branch._transport.put_bytes('tags',
 
162
                self._serialize_tag_dict(new_dict))
 
163
        finally:
 
164
            self.branch.unlock()
227
165
 
228
166
    def _serialize_tag_dict(self, tag_dict):
229
167
        td = dict((k.encode('utf-8'), v)
245
183
            raise ValueError("failed to deserialize tag dictionary %r: %s"
246
184
                % (tag_content, e))
247
185
 
248
 
    def merge_to(self, to_tags, overwrite=False, ignore_master=False):
 
186
    def merge_to(self, to_tags, overwrite=False):
249
187
        """Copy tags between repositories if necessary and possible.
250
 
 
251
 
        This method has common command-line behaviour about handling
 
188
        
 
189
        This method has common command-line behaviour about handling 
252
190
        error cases.
253
191
 
254
192
        All new definitions are copied across, except that tags that already
256
194
 
257
195
        :param to_tags: Branch to receive these tags
258
196
        :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).
261
 
            New in bzr 2.3.
262
197
 
263
 
        :returns: Tuple with tag_updates and tag_conflicts.
264
 
            tag_updates is a dictionary with new tags, None is used for
265
 
            removed tags
266
 
            tag_conflicts is a set of tags that conflicted, each of which is
 
198
        :returns: A list of tags that conflicted, each of which is 
267
199
            (tagname, source_target, dest_target), or None if no copying was
268
200
            done.
269
201
        """
270
 
        operation = cleanup.OperationWithCleanups(self._merge_to_operation)
271
 
        return operation.run(to_tags, overwrite, ignore_master)
272
 
 
273
 
    def _merge_to_operation(self, operation, to_tags, overwrite, ignore_master):
274
 
        add_cleanup = operation.add_cleanup
275
202
        if self.branch == to_tags.branch:
276
 
            return {}, []
277
 
        if not self.branch.supports_tags():
 
203
            return
 
204
        if not self.supports_tags():
278
205
            # obviously nothing to copy
279
 
            return {}, []
 
206
            return
280
207
        source_dict = self.get_tag_dict()
281
208
        if not source_dict:
282
209
            # no tags in the source, and we don't want to clobber anything
283
210
            # that's in the destination
284
 
            return {}, []
285
 
        # We merge_to both master and child individually.
286
 
        #
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.
293
 
        #
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)
298
 
        if ignore_master:
299
 
            master = None
300
 
        else:
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
311
 
        # branch.
312
 
        return updates, set(conflicts)
313
 
 
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
321
 
 
322
 
    def rename_revisions(self, rename_map):
323
 
        """Rename revisions in this tags dictionary.
324
 
 
325
 
        :param rename_map: Dictionary mapping old revids to new revids
326
 
        """
327
 
        reverse_tags = self.get_reverse_tag_dict()
328
 
        for revid, names in reverse_tags.iteritems():
329
 
            if revid in rename_map:
330
 
                for name in names:
331
 
                    self.set_tag(name, rename_map[revid])
 
211
            return
 
212
        to_tags.branch.lock_write()
 
213
        try:
 
214
            dest_dict = to_tags.get_tag_dict()
 
215
            result, conflicts = self._reconcile_tags(source_dict, dest_dict,
 
216
                                                     overwrite)
 
217
            if result != dest_dict:
 
218
                to_tags._set_tag_dict(result)
 
219
        finally:
 
220
            to_tags.branch.unlock()
 
221
        return conflicts
332
222
 
333
223
    def _reconcile_tags(self, source_dict, dest_dict, overwrite):
334
224
        """Do a two-way merge of two tag dictionaries.
335
225
 
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
 
226
        only in source => source value
 
227
        only in destination => destination value
 
228
        same definitions => that
 
229
        different definitions => if overwrite is False, keep destination
 
230
            value and give a warning, otherwise use the source value
341
231
 
342
 
        :returns: (result_dict, updates,
 
232
        :returns: (result_dict,
343
233
            [(conflicting_tag, source_target, dest_target)])
344
234
        """
345
235
        conflicts = []
346
 
        updates = {}
347
236
        result = dict(dest_dict) # copy
348
237
        for name, target in source_dict.items():
349
 
            if result.get(name) == target:
 
238
            if name not in result or overwrite:
 
239
                result[name] = target
 
240
            elif result[name] == target:
350
241
                pass
351
 
            elif name not in result or overwrite:
352
 
                updates[name] = target
353
 
                result[name] = target
354
242
            else:
355
243
                conflicts.append((name, target, result[name]))
356
 
        return result, updates, conflicts
357
 
 
358
 
 
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)
365
 
        return
366
 
    # If the flag is set, try to pass it, but be ready to catch TypeError.
367
 
    try:
368
 
        from_branch.tags.merge_to(to_branch.tags, ignore_master=ignore_master)
369
 
    except TypeError:
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,),),
380
 
            DeprecationWarning)
381
 
        from_branch.tags.merge_to(to_branch.tags)
382
 
 
383
 
 
384
 
def sort_natural(branch, tags):
385
 
    """Sort tags, with numeric substrings as numbers.
386
 
 
387
 
    :param branch: Branch
388
 
    :param tags: List of tuples with tag name and revision id.
389
 
    """
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)
395
 
 
396
 
 
397
 
def sort_alpha(branch, tags):
398
 
    """Sort tags lexicographically, in place.
399
 
 
400
 
    :param branch: Branch
401
 
    :param tags: List of tuples with tag name and revision id.
402
 
    """
403
 
    tags.sort()
404
 
 
405
 
 
406
 
def sort_time(branch, tags):
407
 
    """Sort tags by time inline.
408
 
 
409
 
    :param branch: Branch
410
 
    :param tags: List of tuples with tag name and revision id.
411
 
    """
412
 
    timestamps = {}
413
 
    for tag, revid in tags:
414
 
        try:
415
 
            revobj = branch.repository.get_revision(revid)
416
 
        except errors.NoSuchRevision:
417
 
            timestamp = sys.maxint # place them at the end
418
 
        else:
419
 
            timestamp = revobj.timestamp
420
 
        timestamps[revid] = timestamp
421
 
    tags.sort(key=lambda x: timestamps[x[1]])
422
 
 
423
 
 
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"
 
244
        return result, conflicts
 
245
 
 
246
 
 
247
def _merge_tags_if_possible(from_branch, to_branch):
 
248
    from_branch.tags.merge_to(to_branch.tags)