~bzr-pqm/bzr/bzr.dev

4797.32.2 by John Arbash Meinel
merge 2.1, resolving NEWS conflict.
1
# Copyright (C) 2009, 2010 Canonical Ltd
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
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
6379.6.7 by Jelmer Vernooij
Move importing from future until after doc string, otherwise the doc string will disappear.
17
"""Tools for dealing with the Launchpad API."""
18
6379.6.3 by Jelmer Vernooij
Use absolute_import.
19
from __future__ import absolute_import
20
4505.6.11 by Jonathan Lange
Flag lp_api as a difficult import.
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
4505.6.18 by Jonathan Lange
Another review comment to make a note of.
25
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
26
import os
4969.2.7 by Aaron Bentley
Add lp-submit, get working.
27
import re
5546.2.1 by Aaron Bentley
Add lp-find-proposal.
28
import urlparse
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
29
30
from bzrlib import (
4969.2.7 by Aaron Bentley
Add lp-submit, get working.
31
    branch,
4505.6.24 by Jonathan Lange
Move cache directory to the Bazaar configuration directory.
32
    config,
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
33
    errors,
4505.6.16 by Jonathan Lange
Work on Windows, I think.
34
    osutils,
4969.2.11 by Aaron Bentley
Clean up imports.
35
    trace,
36
    transport,
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
37
    )
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
38
from bzrlib.i18n import gettext
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
39
from bzrlib.plugins.launchpad.lp_registration import (
40
    InvalidLaunchpadInstance,
41
    )
42
4505.6.25 by Jonathan Lange
Add a test to check what happens if launchpadlib not available.
43
try:
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
44
    import launchpadlib
4505.6.25 by Jonathan Lange
Add a test to check what happens if launchpadlib not available.
45
except ImportError, e:
46
    raise errors.DependencyNotPresent('launchpadlib', e)
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
47
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
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
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
59
4505.6.15 by Jonathan Lange
Baby steps: Move the cache directory stuff into a function.
60
def get_cache_directory():
61
    """Return the directory to cache launchpadlib objects in."""
4505.6.24 by Jonathan Lange
Move cache directory to the Bazaar configuration directory.
62
    return osutils.pathjoin(config.config_dir(), 'launchpad')
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
63
64
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
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
4797.84.2 by Max Bowsher
Expand comment.
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.
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
92
LAUNCHPAD_API_URLS = {
4797.84.1 by Max Bowsher
Fix broken communication with the Launchpad web service API when used with
93
    'production': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
94
        'api.launchpad.net'),
5641.1.1 by Max Bowsher
Merge 2.3 into trunk including fix for lp:707075.
95
    'qastaging': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
96
        'api.qastaging.launchpad.net'),
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
97
    'staging': STAGING_SERVICE_ROOT,
4797.84.1 by Max Bowsher
Fix broken communication with the Launchpad web service API when used with
98
    'dev': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
99
        'api.launchpad.dev'),
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
100
    }
101
102
103
def _get_api_url(service):
104
    """Return the root URL of the Launchpad API.
105
4797.76.5 by Vincent Ladeuil
Fix edge references in lp_api and more comments.
106
    e.g. For the 'staging' Launchpad service, this function returns
107
    launchpadlib.launchpad.STAGING_SERVICE_ROOT.
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
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
5546.2.3 by Aaron Bentley
Tighten revno check, avoid creating branches on lp.
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
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
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
    """
4505.6.19 by Jonathan Lange
Delete swathes of code because we can rely on a version of launchpadlib
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.
6015.50.1 by Martin Pool
Use a chmod wrapper to cope with eperm from chmod
140
    osutils.chmod_if_possible(cache_directory, 0700)
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
141
    return launchpad
142
143
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
144
class LaunchpadBranch(object):
4969.2.10 by Aaron Bentley
Cleanup and docs.
145
    """Provide bzr and lp API access to a Launchpad branch."""
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
146
147
    def __init__(self, lp_branch, bzr_url, bzr_branch=None, check_update=True):
4969.2.10 by Aaron Bentley
Cleanup and docs.
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
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
154
        self.bzr_url = bzr_url
155
        self._bzr = bzr_branch
156
        self._push_bzr = None
4969.2.14 by Aaron Bentley
Restore update functionality.
157
        self._check_update = check_update
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
158
        self.lp = lp_branch
159
160
    @property
161
    def bzr(self):
4969.2.10 by Aaron Bentley
Cleanup and docs.
162
        """Return the bzr branch for this branch."""
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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):
4969.2.10 by Aaron Bentley
Cleanup and docs.
169
        """Return the push branch for this branch."""
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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):
4969.2.10 by Aaron Bentley
Cleanup and docs.
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
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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):
4969.2.10 by Aaron Bentley
Cleanup and docs.
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
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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
5657.1.1 by Max Bowsher
Fix bzr lp-mirror to work on command line branch URLs and branches
202
        url = bzr_branch.get_parent()
203
        if url is not None:
204
            yield url
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
205
        yield bzr_branch.base
206
207
    @staticmethod
208
    def tweak_url(url, launchpad):
4969.2.10 by Aaron Bentley
Cleanup and docs.
209
        """Adjust a URL to work with staging, if needed."""
5615.2.1 by Jelmer Vernooij
Support the 'qastaging' instance of Launchpad.
210
        if str(launchpad._root_uri) == STAGING_SERVICE_ROOT:
211
            return url.replace('bazaar.launchpad.net',
212
                               'bazaar.staging.launchpad.net')
5641.1.1 by Max Bowsher
Merge 2.3 into trunk including fix for lp:707075.
213
        elif str(launchpad._root_uri) == LAUNCHPAD_API_URLS['qastaging']:
5615.2.1 by Jelmer Vernooij
Support the 'qastaging' instance of Launchpad.
214
            return url.replace('bazaar.launchpad.net',
215
                               'bazaar.qastaging.launchpad.net')
216
        return url
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
217
218
    @classmethod
5546.2.3 by Aaron Bentley
Tighten revno check, avoid creating branches on lp.
219
    def from_bzr(cls, launchpad, bzr_branch, create_missing=True):
4969.2.10 by Aaron Bentley
Cleanup and docs.
220
        """Find a Launchpad branch from a bzr branch."""
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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:
5546.2.3 by Aaron Bentley
Tighten revno check, avoid creating branches on lp.
230
            if not create_missing:
231
                raise NoLaunchpadBranch(bzr_branch)
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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):
4969.2.10 by Aaron Bentley
Cleanup and docs.
238
        """Create a Bazaar branch on Launchpad for the supplied branch."""
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
239
        url = cls.tweak_url(bzr_branch.get_push_location(), launchpad)
240
        if not cls.plausible_launchpad_url(url):
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
241
            raise errors.BzrError(gettext('%s is not registered on Launchpad') %
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses 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:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
246
            raise errors.BzrError(gettext('%s is not registered on Launchpad') %
247
                                                                            url)
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
248
        return lp_branch
249
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
250
    def get_target(self):
251
        """Return the 'LaunchpadBranch' for the target of this one."""
4969.2.5 by Aaron Bentley
It makes more sense to get the dev focus from an existing Launchpad branch
252
        lp_branch = self.lp
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
253
        if lp_branch.project is not None:
5616.1.2 by Vincent Ladeuil
Fix normal branch usage with lp-propose.
254
            dev_focus = lp_branch.project.development_focus
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
255
            if dev_focus is None:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
256
                raise errors.BzrError(gettext('%s has no development focus.') %
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
257
                                  lp_branch.bzr_identity)
258
            target = dev_focus.branch
259
            if target is None:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
260
                raise errors.BzrError(gettext(
261
                        'development focus %s has no branch.') % dev_focus)
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
262
        elif lp_branch.sourcepackage is not None:
263
            target = lp_branch.sourcepackage.getBranch(pocket="Release")
264
            if target is None:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
265
                raise errors.BzrError(gettext(
266
                                      'source package %s has no branch.') %
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
267
                                      lp_branch.sourcepackage)
268
        else:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
269
            raise errors.BzrError(gettext(
270
                        '%s has no associated product or source package.') %
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
271
                                  lp_branch.bzr_identity)
272
        return LaunchpadBranch(target, target.bzr_identity)
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
273
274
    def update_lp(self):
4969.2.15 by Aaron Bentley
Update docs.
275
        """Update the Launchpad copy of this branch."""
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
282
                    trace.note(gettext('%s is already up-to-date.') %
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
283
                               self.lp.bzr_identity)
284
                    return
285
                graph = self.bzr.repository.get_graph()
4969.2.18 by Aaron Bentley
Fix divergence check.
286
                if not graph.is_ancestor(self.lp.last_scanned_id,
287
                                         self.bzr.last_revision()):
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
288
                    raise errors.DivergedBranches(self.bzr, self.push_bzr)
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
289
                trace.note(gettext('Pushing to %s') % self.lp.bzr_identity)
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
290
            self.bzr.push(self.push_bzr)
291
        finally:
292
            self.bzr.unlock()
293
294
    def find_lca_tree(self, other):
4969.2.10 by Aaron Bentley
Cleanup and docs.
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
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
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
5546.2.1 by Aaron Bentley
Add lp-find-proposal.
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))