/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/plugins/launchpad/lp_api.py

  • Committer: Vincent Ladeuil
  • Date: 2012-01-18 14:09:19 UTC
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120118140919-rlvdrhpc0nq1lbwi
Change set/remove to require a lock for the branch config files.

This means that tests (or any plugin for that matter) do not requires an
explicit lock on the branch anymore to change a single option. This also
means the optimisation becomes "opt-in" and as such won't be as
spectacular as it may be and/or harder to get right (nothing fails
anymore).

This reduces the diff by ~300 lines.

Code/tests that were updating more than one config option is still taking
a lock to at least avoid some IOs and demonstrate the benefits through
the decreased number of hpss calls.

The duplication between BranchStack and BranchOnlyStack will be removed
once the same sharing is in place for local config files, at which point
the Stack class itself may be able to host the changes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2009, 2010 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
"""Tools for dealing with the Launchpad API."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
# Importing this module will be expensive, since it imports launchpadlib and
 
22
# its dependencies. However, our plan is to only load this module when it is
 
23
# needed by a command that uses it.
 
24
 
 
25
 
 
26
import os
 
27
import re
 
28
import urlparse
 
29
 
 
30
from bzrlib import (
 
31
    branch,
 
32
    config,
 
33
    errors,
 
34
    osutils,
 
35
    trace,
 
36
    transport,
 
37
    )
 
38
from bzrlib.i18n import gettext
 
39
from bzrlib.plugins.launchpad.lp_registration import (
 
40
    InvalidLaunchpadInstance,
 
41
    )
 
42
 
 
43
try:
 
44
    import launchpadlib
 
45
except ImportError, e:
 
46
    raise errors.DependencyNotPresent('launchpadlib', e)
 
47
 
 
48
from launchpadlib.launchpad import (
 
49
    STAGING_SERVICE_ROOT,
 
50
    Launchpad,
 
51
    )
 
52
 
 
53
 
 
54
# Declare the minimum version of launchpadlib that we need in order to work.
 
55
# 1.5.1 is the version of launchpadlib packaged in Ubuntu 9.10, the most
 
56
# recent Ubuntu release at the time of writing.
 
57
MINIMUM_LAUNCHPADLIB_VERSION = (1, 5, 1)
 
58
 
 
59
 
 
60
def get_cache_directory():
 
61
    """Return the directory to cache launchpadlib objects in."""
 
62
    return osutils.pathjoin(config.config_dir(), 'launchpad')
 
63
 
 
64
 
 
65
def parse_launchpadlib_version(version_number):
 
66
    """Parse a version number of the style used by launchpadlib."""
 
67
    return tuple(map(int, version_number.split('.')))
 
68
 
 
69
 
 
70
def check_launchpadlib_compatibility():
 
71
    """Raise an error if launchpadlib has the wrong version number."""
 
72
    installed_version = parse_launchpadlib_version(launchpadlib.__version__)
 
73
    if installed_version < MINIMUM_LAUNCHPADLIB_VERSION:
 
74
        raise errors.IncompatibleAPI(
 
75
            'launchpadlib', MINIMUM_LAUNCHPADLIB_VERSION,
 
76
            installed_version, installed_version)
 
77
 
 
78
 
 
79
# The older versions of launchpadlib only provided service root constants for
 
80
# edge and staging, whilst newer versions drop edge. Therefore service root
 
81
# URIs for which we do not always have constants are derived from the staging
 
82
# one, which does always exist.
 
83
#
 
84
# It is necessary to derive, rather than use hardcoded URIs because
 
85
# launchpadlib <= 1.5.4 requires service root URIs that end in a path of
 
86
# /beta/, whilst launchpadlib >= 1.5.5 requires service root URIs with no path
 
87
# info.
 
88
#
 
89
# Once we have a hard dependency on launchpadlib >= 1.5.4 we can replace all of
 
90
# bzr's local knowledge of individual Launchpad instances with use of the
 
91
# launchpadlib.uris module.
 
92
LAUNCHPAD_API_URLS = {
 
93
    'production': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
 
94
        'api.launchpad.net'),
 
95
    'qastaging': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
 
96
        'api.qastaging.launchpad.net'),
 
97
    'staging': STAGING_SERVICE_ROOT,
 
98
    'dev': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
 
99
        'api.launchpad.dev'),
 
100
    }
 
101
 
 
102
 
 
103
def _get_api_url(service):
 
104
    """Return the root URL of the Launchpad API.
 
105
 
 
106
    e.g. For the 'staging' Launchpad service, this function returns
 
107
    launchpadlib.launchpad.STAGING_SERVICE_ROOT.
 
108
 
 
109
    :param service: A `LaunchpadService` object.
 
110
    :return: A URL as a string.
 
111
    """
 
112
    if service._lp_instance is None:
 
113
        lp_instance = service.DEFAULT_INSTANCE
 
114
    else:
 
115
        lp_instance = service._lp_instance
 
116
    try:
 
117
        return LAUNCHPAD_API_URLS[lp_instance]
 
118
    except KeyError:
 
119
        raise InvalidLaunchpadInstance(lp_instance)
 
120
 
 
121
 
 
122
class NoLaunchpadBranch(errors.BzrError):
 
123
    _fmt = 'No launchpad branch could be found for branch "%(url)s".'
 
124
 
 
125
    def __init__(self, branch):
 
126
        errors.BzrError.__init__(self, branch=branch, url=branch.base)
 
127
 
 
128
 
 
129
def login(service, timeout=None, proxy_info=None):
 
130
    """Log in to the Launchpad API.
 
131
 
 
132
    :return: The root `Launchpad` object from launchpadlib.
 
133
    """
 
134
    cache_directory = get_cache_directory()
 
135
    launchpad = Launchpad.login_with(
 
136
        'bzr', _get_api_url(service), cache_directory, timeout=timeout,
 
137
        proxy_info=proxy_info)
 
138
    # XXX: Work-around a minor security bug in launchpadlib 1.5.1, which would
 
139
    # create this directory with default umask.
 
140
    osutils.chmod_if_possible(cache_directory, 0700)
 
141
    return launchpad
 
142
 
 
143
 
 
144
class LaunchpadBranch(object):
 
145
    """Provide bzr and lp API access to a Launchpad branch."""
 
146
 
 
147
    def __init__(self, lp_branch, bzr_url, bzr_branch=None, check_update=True):
 
148
        """Constructor.
 
149
 
 
150
        :param lp_branch: The Launchpad branch.
 
151
        :param bzr_url: The URL of the Bazaar branch.
 
152
        :param bzr_branch: An instance of the Bazaar branch.
 
153
        """
 
154
        self.bzr_url = bzr_url
 
155
        self._bzr = bzr_branch
 
156
        self._push_bzr = None
 
157
        self._check_update = check_update
 
158
        self.lp = lp_branch
 
159
 
 
160
    @property
 
161
    def bzr(self):
 
162
        """Return the bzr branch for this branch."""
 
163
        if self._bzr is None:
 
164
            self._bzr = branch.Branch.open(self.bzr_url)
 
165
        return self._bzr
 
166
 
 
167
    @property
 
168
    def push_bzr(self):
 
169
        """Return the push branch for this branch."""
 
170
        if self._push_bzr is None:
 
171
            self._push_bzr = branch.Branch.open(self.lp.bzr_identity)
 
172
        return self._push_bzr
 
173
 
 
174
    @staticmethod
 
175
    def plausible_launchpad_url(url):
 
176
        """Is 'url' something that could conceivably be pushed to LP?
 
177
 
 
178
        :param url: A URL that may refer to a Launchpad branch.
 
179
        :return: A boolean.
 
180
        """
 
181
        if url is None:
 
182
            return False
 
183
        if url.startswith('lp:'):
 
184
            return True
 
185
        regex = re.compile('([a-z]*\+)*(bzr\+ssh|http)'
 
186
                           '://bazaar.*.launchpad.net')
 
187
        return bool(regex.match(url))
 
188
 
 
189
    @staticmethod
 
190
    def candidate_urls(bzr_branch):
 
191
        """Iterate through related URLs that might be Launchpad URLs.
 
192
 
 
193
        :param bzr_branch: A Bazaar branch to find URLs from.
 
194
        :return: a generator of URL strings.
 
195
        """
 
196
        url = bzr_branch.get_public_branch()
 
197
        if url is not None:
 
198
            yield url
 
199
        url = bzr_branch.get_push_location()
 
200
        if url is not None:
 
201
            yield url
 
202
        url = bzr_branch.get_parent()
 
203
        if url is not None:
 
204
            yield url
 
205
        yield bzr_branch.base
 
206
 
 
207
    @staticmethod
 
208
    def tweak_url(url, launchpad):
 
209
        """Adjust a URL to work with staging, if needed."""
 
210
        if str(launchpad._root_uri) == STAGING_SERVICE_ROOT:
 
211
            return url.replace('bazaar.launchpad.net',
 
212
                               'bazaar.staging.launchpad.net')
 
213
        elif str(launchpad._root_uri) == LAUNCHPAD_API_URLS['qastaging']:
 
214
            return url.replace('bazaar.launchpad.net',
 
215
                               'bazaar.qastaging.launchpad.net')
 
216
        return url
 
217
 
 
218
    @classmethod
 
219
    def from_bzr(cls, launchpad, bzr_branch, create_missing=True):
 
220
        """Find a Launchpad branch from a bzr branch."""
 
221
        check_update = True
 
222
        for url in cls.candidate_urls(bzr_branch):
 
223
            url = cls.tweak_url(url, launchpad)
 
224
            if not cls.plausible_launchpad_url(url):
 
225
                continue
 
226
            lp_branch = launchpad.branches.getByUrl(url=url)
 
227
            if lp_branch is not None:
 
228
                break
 
229
        else:
 
230
            if not create_missing:
 
231
                raise NoLaunchpadBranch(bzr_branch)
 
232
            lp_branch = cls.create_now(launchpad, bzr_branch)
 
233
            check_update = False
 
234
        return cls(lp_branch, bzr_branch.base, bzr_branch, check_update)
 
235
 
 
236
    @classmethod
 
237
    def create_now(cls, launchpad, bzr_branch):
 
238
        """Create a Bazaar branch on Launchpad for the supplied branch."""
 
239
        url = cls.tweak_url(bzr_branch.get_push_location(), launchpad)
 
240
        if not cls.plausible_launchpad_url(url):
 
241
            raise errors.BzrError(gettext('%s is not registered on Launchpad') %
 
242
                                  bzr_branch.base)
 
243
        bzr_branch.create_clone_on_transport(transport.get_transport(url))
 
244
        lp_branch = launchpad.branches.getByUrl(url=url)
 
245
        if lp_branch is None:
 
246
            raise errors.BzrError(gettext('%s is not registered on Launchpad') %
 
247
                                                                            url)
 
248
        return lp_branch
 
249
 
 
250
    def get_target(self):
 
251
        """Return the 'LaunchpadBranch' for the target of this one."""
 
252
        lp_branch = self.lp
 
253
        if lp_branch.project is not None:
 
254
            dev_focus = lp_branch.project.development_focus
 
255
            if dev_focus is None:
 
256
                raise errors.BzrError(gettext('%s has no development focus.') %
 
257
                                  lp_branch.bzr_identity)
 
258
            target = dev_focus.branch
 
259
            if target is None:
 
260
                raise errors.BzrError(gettext(
 
261
                        'development focus %s has no branch.') % dev_focus)
 
262
        elif lp_branch.sourcepackage is not None:
 
263
            target = lp_branch.sourcepackage.getBranch(pocket="Release")
 
264
            if target is None:
 
265
                raise errors.BzrError(gettext(
 
266
                                      'source package %s has no branch.') %
 
267
                                      lp_branch.sourcepackage)
 
268
        else:
 
269
            raise errors.BzrError(gettext(
 
270
                        '%s has no associated product or source package.') %
 
271
                                  lp_branch.bzr_identity)
 
272
        return LaunchpadBranch(target, target.bzr_identity)
 
273
 
 
274
    def update_lp(self):
 
275
        """Update the Launchpad copy of this branch."""
 
276
        if not self._check_update:
 
277
            return
 
278
        self.bzr.lock_read()
 
279
        try:
 
280
            if self.lp.last_scanned_id is not None:
 
281
                if self.bzr.last_revision() == self.lp.last_scanned_id:
 
282
                    trace.note(gettext('%s is already up-to-date.') %
 
283
                               self.lp.bzr_identity)
 
284
                    return
 
285
                graph = self.bzr.repository.get_graph()
 
286
                if not graph.is_ancestor(self.lp.last_scanned_id,
 
287
                                         self.bzr.last_revision()):
 
288
                    raise errors.DivergedBranches(self.bzr, self.push_bzr)
 
289
                trace.note(gettext('Pushing to %s') % self.lp.bzr_identity)
 
290
            self.bzr.push(self.push_bzr)
 
291
        finally:
 
292
            self.bzr.unlock()
 
293
 
 
294
    def find_lca_tree(self, other):
 
295
        """Find the revision tree for the LCA of this branch and other.
 
296
 
 
297
        :param other: Another LaunchpadBranch
 
298
        :return: The RevisionTree of the LCA of this branch and other.
 
299
        """
 
300
        graph = self.bzr.repository.get_graph(other.bzr.repository)
 
301
        lca = graph.find_unique_lca(self.bzr.last_revision(),
 
302
                                    other.bzr.last_revision())
 
303
        return self.bzr.repository.revision_tree(lca)
 
304
 
 
305
 
 
306
def canonical_url(object):
 
307
    """Return the canonical URL for a branch."""
 
308
    scheme, netloc, path, params, query, fragment = urlparse.urlparse(
 
309
        str(object.self_link))
 
310
    path = '/'.join(path.split('/')[2:])
 
311
    netloc = netloc.replace('api.', 'code.')
 
312
    return urlparse.urlunparse((scheme, netloc, path, params, query,
 
313
                                fragment))