~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: John Arbash Meinel
  • Date: 2009-06-12 18:05:15 UTC
  • mto: (4371.4.5 vila-better-heads)
  • mto: This revision was merged to the branch mainline in revision 4449.
  • Revision ID: john@arbash-meinel.com-20090612180515-t0cwbjsnve094oik
Add a failing test for handling nodes that are in the same linear chain.

It fails because the ancestry skipping causes us to miss the fact that the two nodes
are actually directly related. We could check at the beginning, as the 
code used to do, but I think that will be incomplete for the more-than-two
heads cases.

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