~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: John Arbash Meinel
  • Date: 2010-08-13 19:08:57 UTC
  • mto: (5050.17.7 2.2)
  • mto: This revision was merged to the branch mainline in revision 5379.
  • Revision ID: john@arbash-meinel.com-20100813190857-mvzwnimrxvm0zimp
Lots of documentation updates.

We had a lot of http links pointing to the old domain. They should
all now be properly updated to the new domain. (only bazaar-vcs.org
entry left is for pqm, which seems to still reside at the old url.)

Also removed one 'TODO' doc entry about switching to binary xdelta, since
we basically did just that with groupcompress.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 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
 
 
18
import urlparse
 
19
import webbrowser
 
20
 
 
21
from bzrlib import (
 
22
    errors,
 
23
    hooks,
 
24
    msgeditor,
 
25
)
 
26
from bzrlib.plugins.launchpad import (
 
27
    lp_api,
 
28
    lp_registration,
 
29
)
 
30
 
 
31
from lazr.restfulclient import errors as restful_errors
 
32
 
 
33
 
 
34
class ProposeMergeHooks(hooks.Hooks):
 
35
    """Hooks for proposing a merge on Launchpad."""
 
36
 
 
37
    def __init__(self):
 
38
        hooks.Hooks.__init__(self)
 
39
        self.create_hook(
 
40
            hooks.HookPoint(
 
41
                'get_prerequisite',
 
42
                "Return the prerequisite branch for proposing as merge.",
 
43
                (2, 1), None),
 
44
        )
 
45
        self.create_hook(
 
46
            hooks.HookPoint(
 
47
                'merge_proposal_body',
 
48
                "Return an initial body for the merge proposal message.",
 
49
                (2, 1), None),
 
50
        )
 
51
 
 
52
 
 
53
class Proposer(object):
 
54
 
 
55
    hooks = ProposeMergeHooks()
 
56
 
 
57
    def __init__(self, tree, source_branch, target_branch, message, reviews,
 
58
                 staging=False, approve=False):
 
59
        """Constructor.
 
60
 
 
61
        :param tree: The working tree for the source branch.
 
62
        :param source_branch: The branch to propose for merging.
 
63
        :param target_branch: The branch to merge into.
 
64
        :param message: The commit message to use.  (May be None.)
 
65
        :param reviews: A list of tuples of reviewer, review type.
 
66
        :param staging: If True, propose the merge against staging instead of
 
67
            production.
 
68
        :param approve: If True, mark the new proposal as approved immediately.
 
69
            This is useful when a project permits some things to be approved
 
70
            by the submitter (e.g. merges between release and deployment
 
71
            branches).
 
72
        """
 
73
        self.tree = tree
 
74
        if staging:
 
75
            lp_instance = 'staging'
 
76
        else:
 
77
            lp_instance = 'edge'
 
78
        service = lp_registration.LaunchpadService(lp_instance=lp_instance)
 
79
        self.launchpad = lp_api.login(service)
 
80
        self.source_branch = lp_api.LaunchpadBranch.from_bzr(
 
81
            self.launchpad, source_branch)
 
82
        if target_branch is None:
 
83
            self.target_branch = self.source_branch.get_dev_focus()
 
84
        else:
 
85
            self.target_branch = lp_api.LaunchpadBranch.from_bzr(
 
86
                self.launchpad, target_branch)
 
87
        self.commit_message = message
 
88
        # XXX: this is where bug lp:583638 could be tackled.
 
89
        if reviews == []:
 
90
            target_reviewer = self.target_branch.lp.reviewer
 
91
            if target_reviewer is None:
 
92
                raise errors.BzrCommandError('No reviewer specified')
 
93
            self.reviews = [(target_reviewer, '')]
 
94
        else:
 
95
            self.reviews = [(self.launchpad.people[reviewer], review_type)
 
96
                            for reviewer, review_type in
 
97
                            reviews]
 
98
        self.approve = approve
 
99
 
 
100
    def get_comment(self, prerequisite_branch):
 
101
        """Determine the initial comment for the merge proposal."""
 
102
        info = ["Source: %s\n" % self.source_branch.lp.bzr_identity]
 
103
        info.append("Target: %s\n" % self.target_branch.lp.bzr_identity)
 
104
        if prerequisite_branch is not None:
 
105
            info.append("Prereq: %s\n" % prerequisite_branch.lp.bzr_identity)
 
106
        for rdata in self.reviews:
 
107
            uniquename = "%s (%s)" % (rdata[0].display_name, rdata[0].name)
 
108
            info.append('Reviewer: %s, type "%s"\n' % (uniquename, rdata[1]))
 
109
        self.source_branch.bzr.lock_read()
 
110
        try:
 
111
            self.target_branch.bzr.lock_read()
 
112
            try:
 
113
                body = self.get_initial_body()
 
114
            finally:
 
115
                self.target_branch.bzr.unlock()
 
116
        finally:
 
117
            self.source_branch.bzr.unlock()
 
118
        initial_comment = msgeditor.edit_commit_message(''.join(info),
 
119
                                                        start_message=body)
 
120
        return initial_comment.strip().encode('utf-8')
 
121
 
 
122
    def get_initial_body(self):
 
123
        """Get a body for the proposal for the user to modify.
 
124
 
 
125
        :return: a str or None.
 
126
        """
 
127
        def list_modified_files():
 
128
            lca_tree = self.source_branch.find_lca_tree(
 
129
                self.target_branch)
 
130
            source_tree = self.source_branch.bzr.basis_tree()
 
131
            files = modified_files(lca_tree, source_tree)
 
132
            return list(files)
 
133
        target_loc = ('bzr+ssh://bazaar.launchpad.net/%s' %
 
134
                       self.target_branch.lp.unique_name)
 
135
        body = None
 
136
        for hook in self.hooks['merge_proposal_body']:
 
137
            body = hook({
 
138
                'tree': self.tree,
 
139
                'target_branch': target_loc,
 
140
                'modified_files_callback': list_modified_files,
 
141
                'old_body': body,
 
142
            })
 
143
        return body
 
144
 
 
145
    def check_proposal(self):
 
146
        """Check that the submission is sensible."""
 
147
        if self.source_branch.lp.self_link == self.target_branch.lp.self_link:
 
148
            raise errors.BzrCommandError(
 
149
                'Source and target branches must be different.')
 
150
        for mp in self.source_branch.lp.landing_targets:
 
151
            if mp.queue_status in ('Merged', 'Rejected'):
 
152
                continue
 
153
            if mp.target_branch.self_link == self.target_branch.lp.self_link:
 
154
                raise errors.BzrCommandError(
 
155
                    'There is already a branch merge proposal: %s' %
 
156
                    canonical_url(mp))
 
157
 
 
158
    def _get_prerequisite_branch(self):
 
159
        hooks = self.hooks['get_prerequisite']
 
160
        prerequisite_branch = None
 
161
        for hook in hooks:
 
162
            prerequisite_branch = hook(
 
163
                {'launchpad': self.launchpad,
 
164
                 'source_branch': self.source_branch,
 
165
                 'target_branch': self.target_branch,
 
166
                 'prerequisite_branch': prerequisite_branch})
 
167
        return prerequisite_branch
 
168
 
 
169
    def call_webservice(self, call, *args, **kwargs):
 
170
        """Make a call to the webservice, wrapping failures.
 
171
        
 
172
        :param call: The call to make.
 
173
        :param *args: *args for the call.
 
174
        :param **kwargs: **kwargs for the call.
 
175
        :return: The result of calling call(*args, *kwargs).
 
176
        """
 
177
        try:
 
178
            return call(*args, **kwargs)
 
179
        except restful_errors.HTTPError, e:
 
180
            error_lines = []
 
181
            for line in e.content.splitlines():
 
182
                if line.startswith('Traceback (most recent call last):'):
 
183
                    break
 
184
                error_lines.append(line)
 
185
            raise Exception(''.join(error_lines))
 
186
 
 
187
    def create_proposal(self):
 
188
        """Perform the submission."""
 
189
        prerequisite_branch = self._get_prerequisite_branch()
 
190
        if prerequisite_branch is None:
 
191
            prereq = None
 
192
        else:
 
193
            prereq = prerequisite_branch.lp
 
194
            prerequisite_branch.update_lp()
 
195
        self.source_branch.update_lp()
 
196
        reviewers = []
 
197
        review_types = []
 
198
        for reviewer, review_type in self.reviews:
 
199
            review_types.append(review_type)
 
200
            reviewers.append(reviewer.self_link)
 
201
        initial_comment = self.get_comment(prerequisite_branch)
 
202
        mp = self.call_webservice(
 
203
            self.source_branch.lp.createMergeProposal,
 
204
            target_branch=self.target_branch.lp,
 
205
            prerequisite_branch=prereq,
 
206
            initial_comment=initial_comment,
 
207
            commit_message=self.commit_message, reviewers=reviewers,
 
208
            review_types=review_types)
 
209
        if self.approve:
 
210
            self.call_webservice(mp.setStatus, status='Approved')
 
211
        webbrowser.open(canonical_url(mp))
 
212
 
 
213
 
 
214
def modified_files(old_tree, new_tree):
 
215
    """Return a list of paths in the new tree with modified contents."""
 
216
    for f, (op, path), c, v, p, n, (ok, k), e in new_tree.iter_changes(
 
217
        old_tree):
 
218
        if c and k == 'file':
 
219
            yield str(path)
 
220
 
 
221
 
 
222
def canonical_url(object):
 
223
    """Return the canonical URL for a branch."""
 
224
    scheme, netloc, path, params, query, fragment = urlparse.urlparse(
 
225
        str(object.self_link))
 
226
    path = '/'.join(path.split('/')[2:])
 
227
    netloc = netloc.replace('api.', 'code.')
 
228
    return urlparse.urlunparse((scheme, netloc, path, params, query,
 
229
                                fragment))