~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Jelmer Vernooij
  • Date: 2011-12-05 14:12:23 UTC
  • mto: This revision was merged to the branch mainline in revision 6348.
  • Revision ID: jelmer@samba.org-20111205141223-8qxae4h37satlzgq
Move more functionality to vf_search.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2009 Canonical Ltd
 
1
# Copyright (C) 2009, 2010 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
22
22
 
23
23
 
24
24
import os
 
25
import re
 
26
import urlparse
25
27
 
26
28
from bzrlib import (
 
29
    branch,
27
30
    config,
28
31
    errors,
29
32
    osutils,
 
33
    trace,
 
34
    transport,
30
35
    )
 
36
from bzrlib.i18n import gettext
31
37
from bzrlib.plugins.launchpad.lp_registration import (
32
38
    InvalidLaunchpadInstance,
33
 
    NotLaunchpadBranch,
34
39
    )
35
40
 
36
41
try:
39
44
    raise errors.DependencyNotPresent('launchpadlib', e)
40
45
 
41
46
from launchpadlib.launchpad import (
42
 
    EDGE_SERVICE_ROOT,
43
47
    STAGING_SERVICE_ROOT,
44
48
    Launchpad,
45
49
    )
70
74
            installed_version, installed_version)
71
75
 
72
76
 
 
77
# The older versions of launchpadlib only provided service root constants for
 
78
# edge and staging, whilst newer versions drop edge. Therefore service root
 
79
# URIs for which we do not always have constants are derived from the staging
 
80
# one, which does always exist.
 
81
#
 
82
# It is necessary to derive, rather than use hardcoded URIs because
 
83
# launchpadlib <= 1.5.4 requires service root URIs that end in a path of
 
84
# /beta/, whilst launchpadlib >= 1.5.5 requires service root URIs with no path
 
85
# info.
 
86
#
 
87
# Once we have a hard dependency on launchpadlib >= 1.5.4 we can replace all of
 
88
# bzr's local knowledge of individual Launchpad instances with use of the
 
89
# launchpadlib.uris module.
73
90
LAUNCHPAD_API_URLS = {
74
 
    'production': 'https://api.launchpad.net/beta/',
75
 
    'edge': EDGE_SERVICE_ROOT,
 
91
    'production': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
 
92
        'api.launchpad.net'),
 
93
    'qastaging': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
 
94
        'api.qastaging.launchpad.net'),
76
95
    'staging': STAGING_SERVICE_ROOT,
77
 
    'dev': 'https://api.launchpad.dev/beta/',
 
96
    'dev': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
 
97
        'api.launchpad.dev'),
78
98
    }
79
99
 
80
100
 
81
101
def _get_api_url(service):
82
102
    """Return the root URL of the Launchpad API.
83
103
 
84
 
    e.g. For the 'edge' Launchpad service, this function returns
85
 
    launchpadlib.launchpad.EDGE_SERVICE_ROOT.
 
104
    e.g. For the 'staging' Launchpad service, this function returns
 
105
    launchpadlib.launchpad.STAGING_SERVICE_ROOT.
86
106
 
87
107
    :param service: A `LaunchpadService` object.
88
108
    :return: A URL as a string.
97
117
        raise InvalidLaunchpadInstance(lp_instance)
98
118
 
99
119
 
 
120
class NoLaunchpadBranch(errors.BzrError):
 
121
    _fmt = 'No launchpad branch could be found for branch "%(url)s".'
 
122
 
 
123
    def __init__(self, branch):
 
124
        errors.BzrError.__init__(self, branch=branch, url=branch.base)
 
125
 
 
126
 
100
127
def login(service, timeout=None, proxy_info=None):
101
128
    """Log in to the Launchpad API.
102
129
 
112
139
    return launchpad
113
140
 
114
141
 
115
 
def load_branch(launchpad, branch):
116
 
    """Return the launchpadlib Branch object corresponding to 'branch'.
117
 
 
118
 
    :param launchpad: The root `Launchpad` object from launchpadlib.
119
 
    :param branch: A `bzrlib.branch.Branch`.
120
 
    :raise NotLaunchpadBranch: If we cannot determine the Launchpad URL of
121
 
        `branch`.
122
 
    :return: A launchpadlib Branch object.
123
 
    """
124
 
    # XXX: This duplicates the "What are possible URLs for the branch that
125
 
    # Launchpad might recognize" logic found in cmd_lp_open.
126
 
 
127
 
    # XXX: This makes multiple roundtrips to Launchpad for what is
128
 
    # conceptually a single operation -- get me the branches that match these
129
 
    # URLs. Unfortunately, Launchpad's support for such operations is poor, so
130
 
    # we have to allow multiple roundtrips.
131
 
    for url in branch.get_public_branch(), branch.get_push_location():
 
142
class LaunchpadBranch(object):
 
143
    """Provide bzr and lp API access to a Launchpad branch."""
 
144
 
 
145
    def __init__(self, lp_branch, bzr_url, bzr_branch=None, check_update=True):
 
146
        """Constructor.
 
147
 
 
148
        :param lp_branch: The Launchpad branch.
 
149
        :param bzr_url: The URL of the Bazaar branch.
 
150
        :param bzr_branch: An instance of the Bazaar branch.
 
151
        """
 
152
        self.bzr_url = bzr_url
 
153
        self._bzr = bzr_branch
 
154
        self._push_bzr = None
 
155
        self._check_update = check_update
 
156
        self.lp = lp_branch
 
157
 
 
158
    @property
 
159
    def bzr(self):
 
160
        """Return the bzr branch for this branch."""
 
161
        if self._bzr is None:
 
162
            self._bzr = branch.Branch.open(self.bzr_url)
 
163
        return self._bzr
 
164
 
 
165
    @property
 
166
    def push_bzr(self):
 
167
        """Return the push branch for this branch."""
 
168
        if self._push_bzr is None:
 
169
            self._push_bzr = branch.Branch.open(self.lp.bzr_identity)
 
170
        return self._push_bzr
 
171
 
 
172
    @staticmethod
 
173
    def plausible_launchpad_url(url):
 
174
        """Is 'url' something that could conceivably be pushed to LP?
 
175
 
 
176
        :param url: A URL that may refer to a Launchpad branch.
 
177
        :return: A boolean.
 
178
        """
 
179
        if url is None:
 
180
            return False
 
181
        if url.startswith('lp:'):
 
182
            return True
 
183
        regex = re.compile('([a-z]*\+)*(bzr\+ssh|http)'
 
184
                           '://bazaar.*.launchpad.net')
 
185
        return bool(regex.match(url))
 
186
 
 
187
    @staticmethod
 
188
    def candidate_urls(bzr_branch):
 
189
        """Iterate through related URLs that might be Launchpad URLs.
 
190
 
 
191
        :param bzr_branch: A Bazaar branch to find URLs from.
 
192
        :return: a generator of URL strings.
 
193
        """
 
194
        url = bzr_branch.get_public_branch()
 
195
        if url is not None:
 
196
            yield url
 
197
        url = bzr_branch.get_push_location()
 
198
        if url is not None:
 
199
            yield url
 
200
        url = bzr_branch.get_parent()
 
201
        if url is not None:
 
202
            yield url
 
203
        yield bzr_branch.base
 
204
 
 
205
    @staticmethod
 
206
    def tweak_url(url, launchpad):
 
207
        """Adjust a URL to work with staging, if needed."""
 
208
        if str(launchpad._root_uri) == STAGING_SERVICE_ROOT:
 
209
            return url.replace('bazaar.launchpad.net',
 
210
                               'bazaar.staging.launchpad.net')
 
211
        elif str(launchpad._root_uri) == LAUNCHPAD_API_URLS['qastaging']:
 
212
            return url.replace('bazaar.launchpad.net',
 
213
                               'bazaar.qastaging.launchpad.net')
 
214
        return url
 
215
 
 
216
    @classmethod
 
217
    def from_bzr(cls, launchpad, bzr_branch, create_missing=True):
 
218
        """Find a Launchpad branch from a bzr branch."""
 
219
        check_update = True
 
220
        for url in cls.candidate_urls(bzr_branch):
 
221
            url = cls.tweak_url(url, launchpad)
 
222
            if not cls.plausible_launchpad_url(url):
 
223
                continue
 
224
            lp_branch = launchpad.branches.getByUrl(url=url)
 
225
            if lp_branch is not None:
 
226
                break
 
227
        else:
 
228
            if not create_missing:
 
229
                raise NoLaunchpadBranch(bzr_branch)
 
230
            lp_branch = cls.create_now(launchpad, bzr_branch)
 
231
            check_update = False
 
232
        return cls(lp_branch, bzr_branch.base, bzr_branch, check_update)
 
233
 
 
234
    @classmethod
 
235
    def create_now(cls, launchpad, bzr_branch):
 
236
        """Create a Bazaar branch on Launchpad for the supplied branch."""
 
237
        url = cls.tweak_url(bzr_branch.get_push_location(), launchpad)
 
238
        if not cls.plausible_launchpad_url(url):
 
239
            raise errors.BzrError(gettext('%s is not registered on Launchpad') %
 
240
                                  bzr_branch.base)
 
241
        bzr_branch.create_clone_on_transport(transport.get_transport(url))
132
242
        lp_branch = launchpad.branches.getByUrl(url=url)
133
 
        if lp_branch:
134
 
            return lp_branch
135
 
    raise NotLaunchpadBranch(url)
 
243
        if lp_branch is None:
 
244
            raise errors.BzrError(gettext('%s is not registered on Launchpad') %
 
245
                                                                            url)
 
246
        return lp_branch
 
247
 
 
248
    def get_target(self):
 
249
        """Return the 'LaunchpadBranch' for the target of this one."""
 
250
        lp_branch = self.lp
 
251
        if lp_branch.project is not None:
 
252
            dev_focus = lp_branch.project.development_focus
 
253
            if dev_focus is None:
 
254
                raise errors.BzrError(gettext('%s has no development focus.') %
 
255
                                  lp_branch.bzr_identity)
 
256
            target = dev_focus.branch
 
257
            if target is None:
 
258
                raise errors.BzrError(gettext(
 
259
                        'development focus %s has no branch.') % dev_focus)
 
260
        elif lp_branch.sourcepackage is not None:
 
261
            target = lp_branch.sourcepackage.getBranch(pocket="Release")
 
262
            if target is None:
 
263
                raise errors.BzrError(gettext(
 
264
                                      'source package %s has no branch.') %
 
265
                                      lp_branch.sourcepackage)
 
266
        else:
 
267
            raise errors.BzrError(gettext(
 
268
                        '%s has no associated product or source package.') %
 
269
                                  lp_branch.bzr_identity)
 
270
        return LaunchpadBranch(target, target.bzr_identity)
 
271
 
 
272
    def update_lp(self):
 
273
        """Update the Launchpad copy of this branch."""
 
274
        if not self._check_update:
 
275
            return
 
276
        self.bzr.lock_read()
 
277
        try:
 
278
            if self.lp.last_scanned_id is not None:
 
279
                if self.bzr.last_revision() == self.lp.last_scanned_id:
 
280
                    trace.note(gettext('%s is already up-to-date.') %
 
281
                               self.lp.bzr_identity)
 
282
                    return
 
283
                graph = self.bzr.repository.get_graph()
 
284
                if not graph.is_ancestor(self.lp.last_scanned_id,
 
285
                                         self.bzr.last_revision()):
 
286
                    raise errors.DivergedBranches(self.bzr, self.push_bzr)
 
287
                trace.note(gettext('Pushing to %s') % self.lp.bzr_identity)
 
288
            self.bzr.push(self.push_bzr)
 
289
        finally:
 
290
            self.bzr.unlock()
 
291
 
 
292
    def find_lca_tree(self, other):
 
293
        """Find the revision tree for the LCA of this branch and other.
 
294
 
 
295
        :param other: Another LaunchpadBranch
 
296
        :return: The RevisionTree of the LCA of this branch and other.
 
297
        """
 
298
        graph = self.bzr.repository.get_graph(other.bzr.repository)
 
299
        lca = graph.find_unique_lca(self.bzr.last_revision(),
 
300
                                    other.bzr.last_revision())
 
301
        return self.bzr.repository.revision_tree(lca)
 
302
 
 
303
 
 
304
def canonical_url(object):
 
305
    """Return the canonical URL for a branch."""
 
306
    scheme, netloc, path, params, query, fragment = urlparse.urlparse(
 
307
        str(object.self_link))
 
308
    path = '/'.join(path.split('/')[2:])
 
309
    netloc = netloc.replace('api.', 'code.')
 
310
    return urlparse.urlunparse((scheme, netloc, path, params, query,
 
311
                                fragment))