~bzr-pqm/bzr/bzr.dev

6538.2.1 by Aaron Bentley
Update to require launchpadlib 1.6.0
1
# Copyright (C) 2009-2012 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
6598.1.1 by Paul Gear
Provide sensible default proxy_info
26
import httplib2
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
27
import os
4969.2.7 by Aaron Bentley
Add lp-submit, get working.
28
import re
5546.2.1 by Aaron Bentley
Add lp-find-proposal.
29
import urlparse
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
30
31
from bzrlib import (
4969.2.7 by Aaron Bentley
Add lp-submit, get working.
32
    branch,
4505.6.24 by Jonathan Lange
Move cache directory to the Bazaar configuration directory.
33
    config,
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
34
    errors,
4505.6.16 by Jonathan Lange
Work on Windows, I think.
35
    osutils,
4969.2.11 by Aaron Bentley
Clean up imports.
36
    trace,
37
    transport,
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
38
    )
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
39
from bzrlib.i18n import gettext
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
40
from bzrlib.plugins.launchpad.lp_registration import (
41
    InvalidLaunchpadInstance,
42
    )
43
4505.6.25 by Jonathan Lange
Add a test to check what happens if launchpadlib not available.
44
try:
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
45
    import launchpadlib
4505.6.25 by Jonathan Lange
Add a test to check what happens if launchpadlib not available.
46
except ImportError, e:
47
    raise errors.DependencyNotPresent('launchpadlib', e)
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
48
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
49
from launchpadlib.launchpad import (
50
    STAGING_SERVICE_ROOT,
51
    Launchpad,
52
    )
6538.2.1 by Aaron Bentley
Update to require launchpadlib 1.6.0
53
from launchpadlib import uris
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
54
55
# Declare the minimum version of launchpadlib that we need in order to work.
6538.2.1 by Aaron Bentley
Update to require launchpadlib 1.6.0
56
# 1.6.0 is the version of launchpadlib packaged in Ubuntu 10.04, the most
57
# recent Ubuntu LTS release supported on the desktop at the time of writing.
58
MINIMUM_LAUNCHPADLIB_VERSION = (1, 6, 0)
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
59
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
60
4505.6.15 by Jonathan Lange
Baby steps: Move the cache directory stuff into a function.
61
def get_cache_directory():
62
    """Return the directory to cache launchpadlib objects in."""
4505.6.24 by Jonathan Lange
Move cache directory to the Bazaar configuration directory.
63
    return osutils.pathjoin(config.config_dir(), 'launchpad')
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
64
65
4505.6.27 by Jonathan Lange
Add some tests to check for version compatibility. Drop tests for
66
def parse_launchpadlib_version(version_number):
67
    """Parse a version number of the style used by launchpadlib."""
68
    return tuple(map(int, version_number.split('.')))
69
70
71
def check_launchpadlib_compatibility():
72
    """Raise an error if launchpadlib has the wrong version number."""
73
    installed_version = parse_launchpadlib_version(launchpadlib.__version__)
74
    if installed_version < MINIMUM_LAUNCHPADLIB_VERSION:
75
        raise errors.IncompatibleAPI(
76
            'launchpadlib', MINIMUM_LAUNCHPADLIB_VERSION,
77
            installed_version, installed_version)
78
79
6538.2.1 by Aaron Bentley
Update to require launchpadlib 1.6.0
80
def lookup_service_root(service_root):
81
    try:
82
        return uris.lookup_service_root(service_root)
83
    except ValueError:
84
        if service_root != 'qastaging':
85
            raise
86
        staging_root = uris.lookup_service_root('staging')
87
        return staging_root.replace('staging', 'qastaging')
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
88
89
90
def _get_api_url(service):
91
    """Return the root URL of the Launchpad API.
92
4797.76.5 by Vincent Ladeuil
Fix edge references in lp_api and more comments.
93
    e.g. For the 'staging' Launchpad service, this function returns
94
    launchpadlib.launchpad.STAGING_SERVICE_ROOT.
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
95
96
    :param service: A `LaunchpadService` object.
97
    :return: A URL as a string.
98
    """
99
    if service._lp_instance is None:
100
        lp_instance = service.DEFAULT_INSTANCE
101
    else:
102
        lp_instance = service._lp_instance
103
    try:
6538.2.1 by Aaron Bentley
Update to require launchpadlib 1.6.0
104
        return lookup_service_root(lp_instance)
105
    except ValueError:
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
106
        raise InvalidLaunchpadInstance(lp_instance)
107
108
5546.2.3 by Aaron Bentley
Tighten revno check, avoid creating branches on lp.
109
class NoLaunchpadBranch(errors.BzrError):
110
    _fmt = 'No launchpad branch could be found for branch "%(url)s".'
111
112
    def __init__(self, branch):
113
        errors.BzrError.__init__(self, branch=branch, url=branch.base)
114
115
6598.1.2 by Paul Gear
Move defaulting of proxy_info inside login
116
def login(service, timeout=None, proxy_info=None,
6538.2.3 by Aaron Bentley
Fix default version for login.
117
          version=Launchpad.DEFAULT_VERSION):
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
118
    """Log in to the Launchpad API.
119
120
    :return: The root `Launchpad` object from launchpadlib.
121
    """
6598.1.2 by Paul Gear
Move defaulting of proxy_info inside login
122
    if proxy_info is None:
123
        proxy_info = httplib2.proxy_info_from_environment('https')
4505.6.19 by Jonathan Lange
Delete swathes of code because we can rely on a version of launchpadlib
124
    cache_directory = get_cache_directory()
125
    launchpad = Launchpad.login_with(
126
        'bzr', _get_api_url(service), cache_directory, timeout=timeout,
6538.2.2 by Aaron Bentley
Look up merge proposals by exact revision-id.
127
        proxy_info=proxy_info, version=version)
6538.2.1 by Aaron Bentley
Update to require launchpadlib 1.6.0
128
    # XXX: Work-around a minor security bug in launchpadlib < 1.6.3, which
129
    # would create this directory with default umask.
6015.50.1 by Martin Pool
Use a chmod wrapper to cope with eperm from chmod
130
    osutils.chmod_if_possible(cache_directory, 0700)
4505.6.6 by Jonathan Lange
Add a command to mirror Launchpad branches now.
131
    return launchpad
132
133
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
134
class LaunchpadBranch(object):
4969.2.10 by Aaron Bentley
Cleanup and docs.
135
    """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
136
137
    def __init__(self, lp_branch, bzr_url, bzr_branch=None, check_update=True):
4969.2.10 by Aaron Bentley
Cleanup and docs.
138
        """Constructor.
139
140
        :param lp_branch: The Launchpad branch.
141
        :param bzr_url: The URL of the Bazaar branch.
142
        :param bzr_branch: An instance of the Bazaar branch.
143
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
144
        self.bzr_url = bzr_url
145
        self._bzr = bzr_branch
146
        self._push_bzr = None
4969.2.14 by Aaron Bentley
Restore update functionality.
147
        self._check_update = check_update
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
148
        self.lp = lp_branch
149
150
    @property
151
    def bzr(self):
4969.2.10 by Aaron Bentley
Cleanup and docs.
152
        """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
153
        if self._bzr is None:
154
            self._bzr = branch.Branch.open(self.bzr_url)
155
        return self._bzr
156
157
    @property
158
    def push_bzr(self):
4969.2.10 by Aaron Bentley
Cleanup and docs.
159
        """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
160
        if self._push_bzr is None:
161
            self._push_bzr = branch.Branch.open(self.lp.bzr_identity)
162
        return self._push_bzr
163
164
    @staticmethod
165
    def plausible_launchpad_url(url):
4969.2.10 by Aaron Bentley
Cleanup and docs.
166
        """Is 'url' something that could conceivably be pushed to LP?
167
168
        :param url: A URL that may refer to a Launchpad branch.
169
        :return: A boolean.
170
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
171
        if url is None:
172
            return False
173
        if url.startswith('lp:'):
174
            return True
175
        regex = re.compile('([a-z]*\+)*(bzr\+ssh|http)'
176
                           '://bazaar.*.launchpad.net')
177
        return bool(regex.match(url))
178
179
    @staticmethod
180
    def candidate_urls(bzr_branch):
4969.2.10 by Aaron Bentley
Cleanup and docs.
181
        """Iterate through related URLs that might be Launchpad URLs.
182
183
        :param bzr_branch: A Bazaar branch to find URLs from.
184
        :return: a generator of URL strings.
185
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
186
        url = bzr_branch.get_public_branch()
187
        if url is not None:
188
            yield url
189
        url = bzr_branch.get_push_location()
190
        if url is not None:
191
            yield url
5657.1.1 by Max Bowsher
Fix bzr lp-mirror to work on command line branch URLs and branches
192
        url = bzr_branch.get_parent()
193
        if url is not None:
194
            yield url
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
195
        yield bzr_branch.base
196
197
    @staticmethod
198
    def tweak_url(url, launchpad):
4969.2.10 by Aaron Bentley
Cleanup and docs.
199
        """Adjust a URL to work with staging, if needed."""
5615.2.1 by Jelmer Vernooij
Support the 'qastaging' instance of Launchpad.
200
        if str(launchpad._root_uri) == STAGING_SERVICE_ROOT:
201
            return url.replace('bazaar.launchpad.net',
202
                               'bazaar.staging.launchpad.net')
6538.2.1 by Aaron Bentley
Update to require launchpadlib 1.6.0
203
        elif str(launchpad._root_uri) == lookup_service_root('qastaging'):
5615.2.1 by Jelmer Vernooij
Support the 'qastaging' instance of Launchpad.
204
            return url.replace('bazaar.launchpad.net',
205
                               'bazaar.qastaging.launchpad.net')
206
        return url
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
207
208
    @classmethod
5546.2.3 by Aaron Bentley
Tighten revno check, avoid creating branches on lp.
209
    def from_bzr(cls, launchpad, bzr_branch, create_missing=True):
4969.2.10 by Aaron Bentley
Cleanup and docs.
210
        """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
211
        check_update = True
212
        for url in cls.candidate_urls(bzr_branch):
213
            url = cls.tweak_url(url, launchpad)
214
            if not cls.plausible_launchpad_url(url):
215
                continue
216
            lp_branch = launchpad.branches.getByUrl(url=url)
217
            if lp_branch is not None:
218
                break
219
        else:
5546.2.3 by Aaron Bentley
Tighten revno check, avoid creating branches on lp.
220
            if not create_missing:
221
                raise NoLaunchpadBranch(bzr_branch)
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
222
            lp_branch = cls.create_now(launchpad, bzr_branch)
223
            check_update = False
224
        return cls(lp_branch, bzr_branch.base, bzr_branch, check_update)
225
226
    @classmethod
227
    def create_now(cls, launchpad, bzr_branch):
4969.2.10 by Aaron Bentley
Cleanup and docs.
228
        """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
229
        url = cls.tweak_url(bzr_branch.get_push_location(), launchpad)
230
        if not cls.plausible_launchpad_url(url):
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
231
            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
232
                                  bzr_branch.base)
233
        bzr_branch.create_clone_on_transport(transport.get_transport(url))
234
        lp_branch = launchpad.branches.getByUrl(url=url)
235
        if lp_branch is None:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
236
            raise errors.BzrError(gettext('%s is not registered on Launchpad') %
237
                                                                            url)
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
238
        return lp_branch
239
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
240
    def get_target(self):
241
        """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
242
        lp_branch = self.lp
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
243
        if lp_branch.project is not None:
5616.1.2 by Vincent Ladeuil
Fix normal branch usage with lp-propose.
244
            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.
245
            if dev_focus is None:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
246
                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.
247
                                  lp_branch.bzr_identity)
248
            target = dev_focus.branch
249
            if target is None:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
250
                raise errors.BzrError(gettext(
251
                        '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.
252
        elif lp_branch.sourcepackage is not None:
253
            target = lp_branch.sourcepackage.getBranch(pocket="Release")
254
            if target is None:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
255
                raise errors.BzrError(gettext(
256
                                      'source package %s has no branch.') %
5616.1.1 by Jelmer Vernooij
Support 'bzr lp-propose' without an explicit target branch for packaging branches.
257
                                      lp_branch.sourcepackage)
258
        else:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
259
            raise errors.BzrError(gettext(
260
                        '%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.
261
                                  lp_branch.bzr_identity)
262
        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
263
264
    def update_lp(self):
4969.2.15 by Aaron Bentley
Update docs.
265
        """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
266
        if not self._check_update:
267
            return
268
        self.bzr.lock_read()
269
        try:
270
            if self.lp.last_scanned_id is not None:
271
                if self.bzr.last_revision() == self.lp.last_scanned_id:
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
272
                    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
273
                               self.lp.bzr_identity)
274
                    return
275
                graph = self.bzr.repository.get_graph()
4969.2.18 by Aaron Bentley
Fix divergence check.
276
                if not graph.is_ancestor(self.lp.last_scanned_id,
277
                                         self.bzr.last_revision()):
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
278
                    raise errors.DivergedBranches(self.bzr, self.push_bzr)
6150.3.1 by Jonathan Riddell
gettext() in launchpad plugin
279
                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
280
            self.bzr.push(self.push_bzr)
281
        finally:
282
            self.bzr.unlock()
283
284
    def find_lca_tree(self, other):
4969.2.10 by Aaron Bentley
Cleanup and docs.
285
        """Find the revision tree for the LCA of this branch and other.
286
287
        :param other: Another LaunchpadBranch
288
        :return: The RevisionTree of the LCA of this branch and other.
289
        """
4969.2.3 by Aaron Bentley
Move LaunchpadBranch to lp_api. Change the interface so that it uses launchpad
290
        graph = self.bzr.repository.get_graph(other.bzr.repository)
291
        lca = graph.find_unique_lca(self.bzr.last_revision(),
292
                                    other.bzr.last_revision())
293
        return self.bzr.repository.revision_tree(lca)
294
295
5546.2.1 by Aaron Bentley
Add lp-find-proposal.
296
def canonical_url(object):
297
    """Return the canonical URL for a branch."""
298
    scheme, netloc, path, params, query, fragment = urlparse.urlparse(
299
        str(object.self_link))
300
    path = '/'.join(path.split('/')[2:])
301
    netloc = netloc.replace('api.', 'code.')
302
    return urlparse.urlunparse((scheme, netloc, path, params, query,
303
                                fragment))