Merge lp:~bac/launchpad/bug-524302 into lp:launchpad/db-devel

Proposed by Brad Crittenden
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~bac/launchpad/bug-524302
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~bac/launchpad/productseries-js
Diff against target: 1830 lines (+1077/-379)
23 files modified
lib/lp/app/templates/base-layout-macros.pt (+5/-2)
lib/lp/code/browser/bazaar.py (+3/-1)
lib/lp/code/browser/branch.py (+3/-2)
lib/lp/code/browser/configure.zcml (+0/-7)
lib/lp/code/interfaces/codeimport.py (+0/-10)
lib/lp/code/javascript/tests/test_productseries_setbranch.js (+3/-3)
lib/lp/code/model/codeimport.py (+1/-49)
lib/lp/code/model/tests/test_codeimport.py (+0/-194)
lib/lp/code/stories/branches/xx-bazaar-home.txt (+1/-1)
lib/lp/code/stories/branches/xx-branchmergeproposals.txt (+4/-1)
lib/lp/code/stories/branches/xx-propose-for-merging.txt (+2/-0)
lib/lp/code/stories/codeimport/xx-codeimport-list.txt (+0/-72)
lib/lp/code/stories/codeimport/xx-codeimport-view.txt (+3/-3)
lib/lp/code/templates/bazaar-index.pt (+1/-1)
lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt (+4/-0)
lib/lp/registry/browser/configure.zcml (+7/-0)
lib/lp/registry/browser/productseries.py (+380/-24)
lib/lp/registry/browser/tests/productseries-setbranch-view.txt (+339/-0)
lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt (+147/-0)
lib/lp/registry/templates/productseries-codesummary.pt (+3/-3)
lib/lp/registry/templates/productseries-linkbranch.pt (+38/-2)
lib/lp/registry/templates/productseries-setbranch.pt (+129/-0)
lib/lp/testing/factory.py (+4/-4)
To merge this branch: bzr merge lp:~bac/launchpad/bug-524302
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code, ui Approve
Edwin Grubbs (community) code ui* Approve
Review via email: mp+22180@code.launchpad.net

Commit message

Create a productseries/+setbranch page for setting/creating/importing a branch for the productseries.

Description of the change

Add productseries/+setbranch view to consolidate many other views dealing with creating/mirroring/importing branches. This view allows a user to do one of those things (though the terminology is hidden) and links the branch to the product series.

The view is not currently navigable from anywhere. Eventually it will replace +linkbranch.

A new view test has been created:

bin/test -vvt productseries-setbranch-views.txt

It may be preferred to roll that test into the productseries-views.txt test but it was expeditious to create it stand alone.

There are some known issues listed in the BRANCH.TODO file.

To demo, create a new project and go to https://launchpad.dev/<newproject>/trunk/+setbranch

To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (31.1 KiB)

Hi Brad,

This interface is a nice improvement. I've only set the series for a
branch once before, and I totally did it wrong because I was on the wrong
form.

Since I don't know if you are planning a followup branch for your
BRANCH.TODO items, I'm marking this:
    needs-fixing

It seems odd that productseries-setbranch.pt is in lp.registry but
productseries-setbranch.js is in lp.code. There are some more
comments below.

-Edwin

>=== modified file 'BRANCH.TODO'
>--- BRANCH.TODO 2010-03-19 07:13:15 +0000
>+++ BRANCH.TODO 2010-03-26 15:28:25 +0000
>@@ -2,3 +2,10 @@
> # landing. There is a test to ensure it is empty in trunk. If there is
> # stuff still here when you are ready to land, the items should probably
> # be converted to bugs so they can be scheduled.
>+
>+TODO:
>+
>+* validation errors give misleading messages
>+* uncaught constraint error on duplicate of code import URL
>+* code.lp.dev/proj/series displays the overview page but it should
>+ direct away from the code vhost
>=== modified file 'lib/lp/registry/browser/productseries.py'
>--- lib/lp/registry/browser/productseries.py 2010-03-23 00:39:45 +0000
>+++ lib/lp/registry/browser/productseries.py 2010-03-26 15:28:25 +0000
>@@ -644,7 +658,340 @@
> self.next_url = canonical_url(product)
>
>
>-class ProductSeriesLinkBranchView(LaunchpadEditFormView):
>+LINK_LP_BZR = 'link-lp-bzr'
>+CREATE_NEW = 'create-new'
>+IMPORT_EXTERNAL = 'import-external'
>+
>+
>+def _getBranchTypeVocabulary():
>+ items = (
>+ (LINK_LP_BZR,
>+ _("Link to a Bazaar branch already on Launchpad")),
>+ (CREATE_NEW,
>+ _("Create a new, empty branch in Launchpad and "
>+ "link to this series")),
>+ (IMPORT_EXTERNAL,
>+ _("Import a branch hosted somewhere else")),
>+ )
>+ terms = [
>+ SimpleTerm(name, name, label) for name, label in items]
>+ return SimpleVocabulary(terms)

Why is this a function instead of a constant? If you are
trying to avoid extra variables defined in the module, you could
just do:
  BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
      SimpleTerm(LINK_LP_BZR, LINK_LP_BZR, 'foo'),
      ...

>+class RevisionControlSystemsExtended(RevisionControlSystems):
>+ """External RCS plus Bazaar."""
>+ BZR = DBItem(99, """
>+ Bazaar
>+
>+ External Bazaar branch.
>+ """)
>+
>+
>+class SetBranchForm(Interface):
>+ """The fields presented on the form for setting a branch."""
>+
>+ use_template(
>+ ICodeImport,
>+ ['cvs_module'])
>+
>+ rcs_type = Choice(title=_("Type of RCS"),
>+ required=False, vocabulary=RevisionControlSystemsExtended,
>+ description=_(
>+ "The version control system to import from. "))
>+
>+ repo_url = URIField(
>+ title=_("Branch URL"), required=True,
>+ description=_("The URL of the branch."),
>+ allowed_schemes=["http", "https"],
>+ allow_userinfo=False,
>+ allow_port=True,
>+ allow_query=False,
>+ allow_fragment=False,
>+ trailing_slash=False)
>+
>+ branch_location = copy_field(
>+ IProductSeries['branch'],
>+ __name__='branch_location',
>+ titl...

review: Needs Fixing
Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the excellent review Edwin. I've incorporated all of your suggestions. Having the view create the rendered items was a little more difficult due to the two different types of vocabularies.

I also took care of the items in my BRANCH.TODO list including not masking widget validation errors (which you also noted) and catching an error condition when an import URL has been requested before to avoid a db IntegrityError.

Finally I added a story test to show the high-level working of the new page.

Revision history for this message
Brad Crittenden (bac) wrote :
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (19.5 KiB)

Hi Brad,

Thanks for making all the changes. This branch is definitely a lot harder than
I thought.

Since this branch is not linked anywhere yet, I would be ok with you deferring
items 2 and 3 below to a followup branch so you can land this before it gets
any bigger. I also have some comments inline, but they shouldn't be too hard
to fix in this branch.

merge-conditional

1. jslint: Lint found in '/home/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/javascript/code/tests/test_productseries_setbranch.js':
    Line 65 character 15: The body of a for in should be wrapped in an if statement to filter unwanted properties from the prototype.
                for (var sub in subscribers) {

    The easiest way to avoid this potential Javascript problem is:
                    Y.each(subscribers, function(sub) {

2. If the Branch name already exists, it appears as if the form does nothing.

3. If there is an existing imported SVN branch on a given URL, the form will
give you the correct error message, but if there is an existing imported BZR
branch on a given URL, it will give you this exception.

Traceback (most recent call last):
  File "/home/egrubbs/canonical/lp-sourcedeps/eggs/zope.publisher-3.10.0-py2.5.egg/zope/publisher/publish.py", line 134, in publish
    result = publication.callObject(request, obj)
  File "/home/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/webapp/publication.py", line 422, in callObject
    return mapply(ob, request.getPositionalArguments(), request)
  File "/home/egrubbs/canonical/lp-sourcedeps/eggs/zope.publisher-3.10.0-py2.5.egg/zope/publisher/publish.py", line 109, in mapply
    return debug_call(obj, args)
  File "/home/egrubbs/canonical/lp-sourcedeps/eggs/zope.publisher-3.10.0-py2.5.egg/zope/publisher/publish.py", line 115, in debug_call
    return obj(*args)
  File "/home/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/webapp/publisher.py", line 274, in __call__
    self.initialize()
  File "/home/egrubbs/canonical/lp-branches/review/lib/canonical/launchpad/webapp/launchpadform.py", line 111, in initialize
    self.form_result = action.success(data)
  File "/home/egrubbs/canonical/lp-sourcedeps/eggs/zope.formlib-3.6.0-py2.5.egg/zope/formlib/form.py", line 606, in success
    return self.success_handler(self.form, self, data)
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/registry/browser/productseries.py", line 993, in update_action
    data['repo_url'])
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/registry/browser/productseries.py", line 1040, in _createBzrBranch
    url=repo_url)
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/code/model/branchnamespace.py", line 103, in createBranch
    implicit_subscription = self.getPrivacySubscriber()
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/code/model/branchnamespace.py", line 343, in getPrivacySubscriber
    rule = self.product.getBranchVisibilityRuleForTeam(self.owner)
  File "/home/egrubbs/canonical/lp-branches/review/lib/lp/code/model/branchvisibilitypolicy.py", line 108, in getBranchVisibilityRuleForTeam
    item = self._selectOneBranchVisibilityTeamPolicy(team)
  File "/home/...

review: Approve (code ui*)
Revision history for this message
Brad Crittenden (bac) wrote :

Edwin,

Thanks for the review and the ideas about cleaning up the render() method.

I am going to defer the other two items for another, quick follow-on branch.

The incremental is at:
http://pastebin.ubuntu.com/409614/

Revision history for this message
Brad Crittenden (bac) wrote :

Curtis a screenshot is available at http://people.canonical.com/~bac/setbranch.png

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Brad.

I think Branch name and owner are confusing. I think they subordinate to importing or creating a new branch but they appear to be enabled for setting the series to an existing branch. I know I cannot change the owner or name for the first option. I expect these two options to be, and appear to be, disbaled when I choose Link to a bazaar branch in Launchpad.

I do not see any test to verify the script is loaded on the page. Either we need to show that the
script is in the page or use windmill to verify it executed.

review: Needs Information (code, ui)
Revision history for this message
Brad Crittenden (bac) wrote :

As we discussed, the branch name and owner are conditionally disabled correctly, though the screen shot does not show it well.

The extra windmill test will be added to the follow up branch.

Revision history for this message
Curtis Hovey (sinzui) :
review: Approve (code, ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
2--- lib/lp/app/templates/base-layout-macros.pt 2010-03-17 23:17:46 +0000
3+++ lib/lp/app/templates/base-layout-macros.pt 2010-04-07 13:24:38 +0000
4@@ -202,8 +202,11 @@
5 tal:attributes="src string:${lp_js}/code/branchmergeproposal.status.js">
6 </script>
7 <script type="text/javascript"
8- tal:attributes="src string:${lp_js}/code/branchmergeproposal.reviewcomment.js"></script>
9-
10+ tal:attributes="src string:${lp_js}/code/branchmergeproposal.reviewcomment.js">
11+ </script>
12+ <script type="text/javascript"
13+ tal:attributes="src string:${lp_js}/code/productseries-setbranch.js">
14+ </script>
15 <script type="text/javascript"
16 tal:attributes="src string:${lp_js}/lp/comment.js"></script>
17 <script type="text/javascript"
18
19=== modified file 'lib/lp/code/browser/bazaar.py'
20--- lib/lp/code/browser/bazaar.py 2009-08-28 01:31:51 +0000
21+++ lib/lp/code/browser/bazaar.py 2010-04-07 13:24:38 +0000
22@@ -21,6 +21,7 @@
23 from canonical.launchpad.webapp.authorization import (
24 precache_permission_for_objects)
25
26+from lp.code.enums import CodeImportReviewStatus
27 from lp.code.interfaces.branch import IBranchCloud, IBranchSet
28 from lp.code.interfaces.branchcollection import IAllBranches
29 from lp.code.interfaces.codeimport import ICodeImportSet
30@@ -61,7 +62,8 @@
31
32 @property
33 def import_count(self):
34- return getUtility(ICodeImportSet).getActiveImports().count()
35+ return getUtility(ICodeImportSet).search(
36+ review_status=CodeImportReviewStatus.REVIEWED).count()
37
38 @property
39 def bzr_version(self):
40
41=== modified file 'lib/lp/code/browser/branch.py'
42--- lib/lp/code/browser/branch.py 2010-03-16 19:04:48 +0000
43+++ lib/lp/code/browser/branch.py 2010-04-07 13:24:38 +0000
44@@ -16,6 +16,7 @@
45 'BranchReviewerEditView',
46 'BranchMergeQueueView',
47 'BranchMirrorStatusView',
48+ 'BranchNameValidationMixin',
49 'BranchNavigation',
50 'BranchEditMenu',
51 'BranchInProductView',
52@@ -612,7 +613,7 @@
53 class BranchNameValidationMixin:
54 """Provide name validation logic used by several branch view classes."""
55
56- def _setBranchExists(self, existing_branch):
57+ def _setBranchExists(self, existing_branch, field_name='name'):
58 owner = existing_branch.owner
59 if owner == self.user:
60 prefix = "You already have"
61@@ -622,7 +623,7 @@
62 "%s a branch for <em>%s</em> called <em>%s</em>."
63 % (prefix, existing_branch.target.displayname,
64 existing_branch.name))
65- self.setFieldError('name', structured(message))
66+ self.setFieldError(field_name, structured(message))
67
68
69 class BranchEditSchema(Interface):
70
71=== modified file 'lib/lp/code/browser/configure.zcml'
72--- lib/lp/code/browser/configure.zcml 2010-03-18 17:30:14 +0000
73+++ lib/lp/code/browser/configure.zcml 2010-04-07 13:24:38 +0000
74@@ -57,13 +57,6 @@
75 permission="zope.Public"
76 />
77 <browser:page
78- for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication"
79- name="+code-import-list"
80- class="lp.registry.browser.productseries.ProductSeriesSourceListView"
81- template="../templates/sources-list.pt"
82- permission="zope.Public"
83- />
84- <browser:page
85 for="zope.interface.Interface"
86 name="+test-webservice-js"
87 template="../../../canonical/launchpad/templates/test-webservice-js.pt"
88
89=== modified file 'lib/lp/code/interfaces/codeimport.py'
90--- lib/lp/code/interfaces/codeimport.py 2010-03-26 01:05:26 +0000
91+++ lib/lp/code/interfaces/codeimport.py 2010-04-07 13:24:38 +0000
92@@ -188,16 +188,6 @@
93 :param target: An `IBranchTarget` that the code is associated with.
94 """
95
96- def getActiveImports(text=None):
97- """Return an iterable of all 'active' CodeImport objects.
98-
99- Active is defined, somewhat arbitrarily, as having
100- review_status==REVIEWED and having completed at least once.
101-
102- :param text: If specifed, limit to the results to those that contain
103- ``text`` in the product or project titles and descriptions.
104- """
105-
106 def get(id):
107 """Get a CodeImport by its id.
108
109
110=== modified file 'lib/lp/code/javascript/tests/test_productseries_setbranch.js'
111--- lib/lp/code/javascript/tests/test_productseries_setbranch.js 2010-04-05 18:58:07 +0000
112+++ lib/lp/code/javascript/tests/test_productseries_setbranch.js 2010-04-07 13:24:38 +0000
113@@ -61,10 +61,10 @@
114 var custom_events = Y.Event.getListeners(field, 'click');
115 var click_event = custom_events[0];
116 var subscribers = click_event.subscribers;
117- for (var sub in subscribers) {
118- Y.Assert.isTrue(subscribers[sub].contains(expected),
119+ Y.each(subscribers, function(sub) {
120+ Y.Assert.isTrue(sub.contains(expected),
121 'branch_type_onclick handler setup');
122- };
123+ });
124 };
125
126 check_handler(this.link_lp_bzr, module.onclick_branch_type);
127
128=== modified file 'lib/lp/code/model/codeimport.py'
129--- lib/lp/code/model/codeimport.py 2010-03-18 15:39:58 +0000
130+++ lib/lp/code/model/codeimport.py 2010-04-07 13:24:38 +0000
131@@ -30,10 +30,9 @@
132 from canonical.database.constants import DEFAULT
133 from canonical.database.datetimecol import UtcDateTimeCol
134 from canonical.database.enumcol import EnumCol
135-from canonical.database.sqlbase import SQLBase, quote, sqlvalues
136+from canonical.database.sqlbase import SQLBase
137 from canonical.launchpad.interfaces import IStore
138 from lp.code.model.codeimportjob import CodeImportJobWorkflow
139-from lp.registry.model.productseries import ProductSeries
140 from canonical.launchpad.webapp.interfaces import NotFoundError
141 from lp.code.enums import (
142 BranchType, CodeImportJobState, CodeImportResultStatus,
143@@ -41,8 +40,6 @@
144 from lp.code.interfaces.codeimport import ICodeImport, ICodeImportSet
145 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
146 from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
147-from lp.code.interfaces.branchnamespace import (
148- get_branch_namespace)
149 from lp.code.model.codeimportresult import CodeImportResult
150 from lp.code.mail.codeimport import code_import_updated
151 from lp.registry.interfaces.person import validate_public_person
152@@ -256,51 +253,6 @@
153 CodeImportJob.delete(code_import.import_job.id)
154 CodeImport.delete(code_import.id)
155
156- def getActiveImports(self, text=None):
157- """See `ICodeImportSet`."""
158- query = self.composeQueryString(text)
159- return CodeImport.select(
160- query, orderBy=['product.name', 'branch.name'],
161- clauseTables=['Product', 'Branch'])
162-
163- def composeQueryString(self, text=None):
164- """Build SQL "where" clause for `CodeImport` search.
165-
166- :param text: Text to search for in the product and project titles and
167- descriptions.
168- """
169- conditions = [
170- "date_last_successful IS NOT NULL",
171- "review_status=%s" % sqlvalues(CodeImportReviewStatus.REVIEWED),
172- "CodeImport.branch = Branch.id",
173- "Branch.product = Product.id",
174- ]
175- if text == u'':
176- text = None
177-
178- # First filter on text, if supplied.
179- if text is not None:
180- conditions.append("""
181- ((Project.fti @@ ftq(%s) AND Product.project IS NOT NULL) OR
182- Product.fti @@ ftq(%s))""" % (quote(text), quote(text)))
183-
184- # Exclude deactivated products.
185- conditions.append('Product.active IS TRUE')
186-
187- # Exclude deactivated projects, too.
188- conditions.append(
189- "((Product.project = Project.id AND Project.active) OR"
190- " Product.project IS NULL)")
191-
192- # And build the query.
193- query = " AND ".join(conditions)
194- return """
195- codeimport.id IN
196- (SELECT codeimport.id FROM codeimport, branch, product, project
197- WHERE %s)
198- AND codeimport.branch = branch.id
199- AND branch.product = product.id""" % query
200-
201 def get(self, id):
202 """See `ICodeImportSet`."""
203 try:
204
205=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
206--- lib/lp/code/model/tests/test_codeimport.py 2010-03-18 17:49:21 +0000
207+++ lib/lp/code/model/tests/test_codeimport.py 2010-04-07 13:24:38 +0000
208@@ -11,14 +11,11 @@
209 from storm.store import Store
210 from zope.component import getUtility
211
212-from lp.codehosting.codeimport.tests.test_workermonitor import (
213- nuke_codeimport_sample_data)
214 from lp.code.model.codeimport import CodeImportSet
215 from lp.code.model.codeimportevent import CodeImportEvent
216 from lp.code.model.codeimportjob import CodeImportJob, CodeImportJobSet
217 from lp.code.model.codeimportresult import CodeImportResult
218 from lp.code.interfaces.branchtarget import IBranchTarget
219-from lp.code.interfaces.codeimport import ICodeImportSet
220 from lp.registry.interfaces.person import IPersonSet
221 from lp.code.enums import (
222 CodeImportResultStatus, CodeImportReviewStatus, RevisionControlSystems)
223@@ -551,196 +548,5 @@
224 requester, code_import.import_job.requesting_user)
225
226
227-def make_active_import(factory, project_name=None, product_name=None,
228- branch_name=None, svn_branch_url=None,
229- cvs_root=None, cvs_module=None, git_repo_url=None,
230- hg_repo_url=None, last_update=None, rcs_type=None):
231- """Make a new CodeImport for a new Product, maybe in a new Project.
232-
233- The import will be 'active' in the sense used by
234- `ICodeImportSet.getActiveImports`.
235- """
236- if project_name is not None:
237- project = factory.makeProject(name=project_name)
238- else:
239- project = None
240- product = factory.makeProduct(
241- name=product_name, displayname=product_name, project=project)
242- code_import = factory.makeProductCodeImport(
243- product=product, branch_name=branch_name,
244- svn_branch_url=svn_branch_url, cvs_root=cvs_root,
245- cvs_module=cvs_module, git_repo_url=git_repo_url,
246- hg_repo_url=hg_repo_url, rcs_type=None)
247- make_import_active(factory, code_import, last_update)
248- return code_import
249-
250-
251-def make_import_active(factory, code_import, last_update=None):
252- """Make `code_import` active as per `ICodeImportSet.getActiveImports`."""
253- from zope.security.proxy import removeSecurityProxy
254- naked_import = removeSecurityProxy(code_import)
255- if naked_import.review_status != CodeImportReviewStatus.REVIEWED:
256- naked_import.updateFromData(
257- {'review_status': CodeImportReviewStatus.REVIEWED},
258- factory.makePerson())
259- if last_update is None:
260- # If last_update is not specfied, presumably we don't care what it is
261- # so we just use some made up value.
262- last_update = datetime(2008, 1, 1, tzinfo=pytz.UTC)
263- naked_import.date_last_successful = last_update
264-
265-
266-def deactivate(project_or_product):
267- """Mark `project_or_product` as not active."""
268- from zope.security.proxy import removeSecurityProxy
269- removeSecurityProxy(project_or_product).active = False
270-
271-
272-class TestGetActiveImports(TestCaseWithFactory):
273- """Tests for CodeImportSet.getActiveImports()."""
274-
275- layer = DatabaseFunctionalLayer
276-
277- def setUp(self):
278- """Prepare by deleting all the import data in the sample data.
279-
280- This means that the tests only have to care about the import
281- data they create.
282- """
283- super(TestGetActiveImports, self).setUp()
284- nuke_codeimport_sample_data()
285- login('no-priv@canonical.com')
286-
287- def tearDown(self):
288- super(TestGetActiveImports, self).tearDown()
289- logout()
290-
291- def testEmpty(self):
292- # We start out with no code imports, so getActiveImports() returns no
293- # results.
294- results = getUtility(ICodeImportSet).getActiveImports()
295- self.assertEquals(list(results), [])
296-
297- def testOneSeries(self):
298- # When there is one active import, it is returned.
299- code_import = make_active_import(self.factory)
300- results = getUtility(ICodeImportSet).getActiveImports()
301- self.assertEquals(list(results), [code_import])
302-
303- def testOneSeriesWithProject(self):
304- # Code imports for products with a project should be returned too.
305- code_import = make_active_import(
306- self.factory, project_name="whatever")
307- results = getUtility(ICodeImportSet).getActiveImports()
308- self.assertEquals(list(results), [code_import])
309-
310- def testExcludeDeactivatedProducts(self):
311- # Deactivating a product means that code imports associated to it are
312- # no longer returned.
313- code_import = make_active_import(self.factory)
314- self.failUnless(code_import.branch.product.active)
315- results = getUtility(ICodeImportSet).getActiveImports()
316- self.assertEquals(list(results), [code_import])
317- deactivate(code_import.branch.product)
318- results = getUtility(ICodeImportSet).getActiveImports()
319- self.assertEquals(list(results), [])
320-
321- def testExcludeDeactivatedProjects(self):
322- # Deactivating a project means that code imports associated to
323- # products in it are no longer returned.
324- code_import = make_active_import(
325- self.factory, project_name="whatever")
326- self.failUnless(code_import.branch.product.project.active)
327- results = getUtility(ICodeImportSet).getActiveImports()
328- self.assertEquals(list(results), [code_import])
329- deactivate(code_import.branch.product.project)
330- results = getUtility(ICodeImportSet).getActiveImports()
331- self.assertEquals(list(results), [])
332-
333- def testSorting(self):
334- # Returned code imports are sorted by product name, then branch name.
335- prod1_a = make_active_import(
336- self.factory, product_name='prod1', branch_name='a')
337- prod2_a = make_active_import(
338- self.factory, product_name='prod2', branch_name='a')
339- prod1_b = self.factory.makeProductCodeImport(
340- product=prod1_a.branch.product, branch_name='b')
341- make_import_active(self.factory, prod1_b)
342- results = getUtility(ICodeImportSet).getActiveImports()
343- self.assertEquals(
344- list(results), [prod1_a, prod1_b, prod2_a])
345-
346- def testSearchByProduct(self):
347- # Searching can filter by product name and other texts.
348- code_import = make_active_import(
349- self.factory, product_name='product')
350- results = getUtility(ICodeImportSet).getActiveImports(
351- text='product')
352- self.assertEquals(
353- list(results), [code_import])
354-
355- def testSearchByProductWithProject(self):
356- # Searching can filter by product name and other texts, and returns
357- # matching imports even if the associated product is in a project
358- # which does not match.
359- code_import = make_active_import(
360- self.factory, project_name='whatever', product_name='product')
361- results = getUtility(ICodeImportSet).getActiveImports(
362- text='product')
363- self.assertEquals(
364- list(results), [code_import])
365-
366- def testSearchByProject(self):
367- # Searching can filter by project name and other texts.
368- code_import = make_active_import(
369- self.factory, project_name='project', product_name='product')
370- results = getUtility(ICodeImportSet).getActiveImports(
371- text='project')
372- self.assertEquals(
373- list(results), [code_import])
374-
375- def testSearchByProjectWithNonMatchingProduct(self):
376- # If a project matches the text, it's an easy mistake to make to
377- # consider all the products with no project as matching too.
378- code_import_1 = make_active_import(
379- self.factory, product_name='product1')
380- code_import_2 = make_active_import(
381- self.factory, project_name='thisone', product_name='product2')
382- results = getUtility(ICodeImportSet).getActiveImports(
383- text='thisone')
384- self.assertEquals(
385- list(results), [code_import_2])
386-
387- def testJoining(self):
388- # Test that the query composed by CodeImportSet.composeQueryString
389- # gets the joins right. We create code imports for each of the
390- # possibilities of active or inactive product and active or inactive
391- # or absent project.
392- expected = set()
393- source = {}
394- for project_active in [True, False, None]:
395- for product_active in [True, False]:
396- if project_active is not None:
397- project_name = self.factory.getUniqueString()
398- else:
399- project_name = None
400- code_import = make_active_import(
401- self.factory, project_name=project_name)
402- if code_import.branch.product.project and not project_active:
403- deactivate(code_import.branch.product.project)
404- if not product_active:
405- deactivate(code_import.branch.product)
406- if project_active != False and product_active:
407- expected.add(code_import)
408- source[code_import] = (product_active, project_active)
409- results = set(getUtility(ICodeImportSet).getActiveImports())
410- errors = []
411- for extra in results - expected:
412- errors.append(('extra', source[extra]))
413- for missing in expected - results:
414- errors.append(('extra', source[missing]))
415- self.assertEquals(errors, [])
416-
417-
418 def test_suite():
419 return unittest.TestLoader().loadTestsFromName(__name__)
420
421=== modified file 'lib/lp/code/stories/branches/xx-bazaar-home.txt'
422--- lib/lp/code/stories/branches/xx-bazaar-home.txt 2009-08-28 05:57:37 +0000
423+++ lib/lp/code/stories/branches/xx-bazaar-home.txt 2010-04-07 13:24:38 +0000
424@@ -16,7 +16,7 @@
425 >>> print extract_text(footer)
426 30 branches registered in
427 6 projects
428- 0 imported branches
429+ 1 imported branches
430 2 branches associated with bug reports
431 Launchpad uses Bazaar 0.92.0.
432
433
434=== modified file 'lib/lp/code/stories/branches/xx-branchmergeproposals.txt'
435--- lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-02-23 21:48:53 +0000
436+++ lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2010-04-07 13:24:38 +0000
437@@ -80,13 +80,16 @@
438 ... print extract_text(find_tag_by_id(
439 ... browser.contents, 'proposal-summary'))
440 >>> print_summary(nopriv_browser)
441- Status:...
442+ Status:
443+ ...
444 Proposed branch:
445 lp://dev/~name12/gnome-terminal/klingon
446 Merge into:
447 lp://dev/~name12/gnome-terminal/main
448 Prerequisite:
449 lp://dev/~name12/gnome-terminal/pushed
450+ To merge this branch:
451+ bzr merge lp://dev/~name12/gnome-terminal/klingon
452
453
454 Editing a commit message
455
456=== modified file 'lib/lp/code/stories/branches/xx-propose-for-merging.txt'
457--- lib/lp/code/stories/branches/xx-propose-for-merging.txt 2010-01-14 04:32:38 +0000
458+++ lib/lp/code/stories/branches/xx-propose-for-merging.txt 2010-04-07 13:24:38 +0000
459@@ -35,6 +35,7 @@
460 Status: Needs review
461 Proposed branch: ...
462 Merge into: lp://dev/fooix
463+ To merge this branch: bzr merge ...
464
465
466 Work in progress
467@@ -57,3 +58,4 @@
468 Status: Work in progress
469 Proposed branch: ...
470 Merge into: lp://dev/fooix
471+ To merge this branch: bzr merge ...
472
473=== removed file 'lib/lp/code/stories/codeimport/xx-codeimport-list.txt'
474--- lib/lp/code/stories/codeimport/xx-codeimport-list.txt 2010-01-12 22:09:23 +0000
475+++ lib/lp/code/stories/codeimport/xx-codeimport-list.txt 1970-01-01 00:00:00 +0000
476@@ -1,72 +0,0 @@
477-There is a page listing all the active code imports on the code
478-homepage.
479-
480-We start by deleting all the code import sample data and creating a
481-few imports that will be displayed in the listing.
482-
483- >>> import datetime
484- >>> import pytz
485- >>> from lp.codehosting.codeimport.tests.test_workermonitor import (
486- ... nuke_codeimport_sample_data)
487- >>> from lp.code.enums import RevisionControlSystems
488- >>> from lp.code.model.tests.test_codeimport import (
489- ... make_active_import)
490- >>> from lp.code.interfaces.codeimport import ICodeImportSet
491- >>> from lp.testing import login, logout
492- >>> from zope.component import getUtility
493- >>> login('david.allouche@canonical.com')
494- >>> nuke_codeimport_sample_data()
495- >>> code_import_set = getUtility(ICodeImportSet)
496- >>> active_svn_import = make_active_import(
497- ... factory, product_name="myproject", branch_name="trunk",
498- ... svn_branch_url="http://example.com/svn/myproject/trunk",
499- ... last_update=datetime.datetime(2007, 1, 1, tzinfo=pytz.UTC))
500- >>> active_bzr_svn_import = make_active_import(
501- ... factory, product_name="ourproject", branch_name="trunk",
502- ... svn_branch_url="http://example.com/bzr-svn/myproject/trunk",
503- ... rcs_type=RevisionControlSystems.BZR_SVN,
504- ... last_update=datetime.datetime(2007, 1, 2, tzinfo=pytz.UTC))
505- >>> active_cvs_import = make_active_import(
506- ... factory, product_name="hisproj", branch_name="main",
507- ... cvs_root=":pserver:anon@example.com:/cvs", cvs_module="hisproj",
508- ... last_update=datetime.datetime(2007, 1, 3, tzinfo=pytz.UTC))
509- >>> active_git_import = make_active_import(
510- ... factory, product_name="herproj", branch_name="master",
511- ... git_repo_url="git://git.example.org/herproj",
512- ... last_update=datetime.datetime(2007, 1, 4, tzinfo=pytz.UTC))
513- >>> active_hg_import = make_active_import(
514- ... factory, product_name="hg-proj", branch_name="tip",
515- ... hg_repo_url="http://hg.example.org/proj",
516- ... last_update=datetime.datetime(2007, 1, 5, tzinfo=pytz.UTC))
517- >>> len(list(code_import_set.getActiveImports()))
518- 5
519- >>> logout()
520-
521-The page is linked to from the "$N imported branches" text.
522-
523- >>> browser.open('http://code.launchpad.dev')
524- >>> browser.getLink('5 imported branches').click()
525- >>> print browser.title
526- Available code imports
527-
528-It lists the active imports, sorted by product then branch name:
529-
530- >>> def print_import_table():
531- ... table = first_tag_by_class(browser.contents, 'listing')
532- ... print extract_text(table)
533-
534- >>> print_import_table()
535- Project Branch Name Source Details Last Updated
536- herproj master git://git.example.org/herproj 2007-01-04
537- hg-proj tip http://hg.example.org/proj 2007-01-05
538- hisproj main :pserver:anon@example.com:/cvs hisproj 2007-01-03
539- myproject trunk http://example.com/svn/myproject/trunk 2007-01-01
540- ourproject trunk http://example.com/bzr-svn/myproject/trunk 2007-01-02
541-
542-You can filter the list by product:
543-
544- >>> browser.getControl(name='text').value = 'hisproj'
545- >>> browser.getControl('Search', index=0).click()
546- >>> print_import_table()
547- Project Branch Name Source Details Last Updated
548- hisproj main :pserver:anon@example.com:/cvs hisproj 2007-01-03
549
550=== modified file 'lib/lp/code/stories/codeimport/xx-codeimport-view.txt'
551--- lib/lp/code/stories/codeimport/xx-codeimport-view.txt 2010-03-18 17:49:21 +0000
552+++ lib/lp/code/stories/codeimport/xx-codeimport-view.txt 2010-04-07 13:24:38 +0000
553@@ -1,10 +1,10 @@
554 Code imports
555 ============
556
557-For now, there is no link to the page that lists all code imports, so
558-we browse there directly:
559+The code imports overview page is linked of the main code page.
560
561- >>> browser.open('http://code.launchpad.dev/+code-imports')
562+ >>> browser.open('http://code.launchpad.dev')
563+ >>> browser.getLink('1 imported branches').click()
564 >>> print browser.title
565 Code Imports
566
567
568=== modified file 'lib/lp/code/templates/bazaar-index.pt'
569--- lib/lp/code/templates/bazaar-index.pt 2010-03-11 06:43:00 +0000
570+++ lib/lp/code/templates/bazaar-index.pt 2010-04-07 13:24:38 +0000
571@@ -121,7 +121,7 @@
572 </a>
573 </div>
574 <div>
575- <a href="/+code-import-list">
576+ <a href="/+code-imports">
577 <strong tal:content="view/import_count">123</strong>
578 imported branches
579 </a>
580
581=== modified file 'lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt'
582--- lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt 2010-02-24 08:05:27 +0000
583+++ lib/lp/code/templates/branchmergeproposal-pagelet-summary.pt 2010-04-07 13:24:38 +0000
584@@ -131,5 +131,9 @@
585 tal:content="context/preview_diff/conflicts"/>
586 </td>
587 </tr>
588+ <tr id="summary-row-merge-instruction">
589+ <th>To merge this branch:</th>
590+ <td>bzr merge <span class="branch-url" tal:content="context/source_branch/bzr_identity" /></td>
591+ </tr>
592 </tbody>
593 </table>
594
595=== modified file 'lib/lp/registry/browser/configure.zcml'
596--- lib/lp/registry/browser/configure.zcml 2010-04-01 18:48:04 +0000
597+++ lib/lp/registry/browser/configure.zcml 2010-04-07 13:24:38 +0000
598@@ -1664,6 +1664,13 @@
599 facet="overview"
600 permission="launchpad.Edit"/>
601 <browser:page
602+ for="lp.registry.interfaces.productseries.IProductSeries"
603+ name="+setbranch"
604+ class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
605+ template="../templates/productseries-setbranch.pt"
606+ facet="overview"
607+ permission="launchpad.Edit"/>
608+ <browser:page
609 name="+review"
610 for="lp.registry.interfaces.productseries.IProductSeries"
611 class="lp.registry.browser.productseries.ProductSeriesReviewView"
612
613=== modified file 'lib/lp/registry/browser/productseries.py'
614--- lib/lp/registry/browser/productseries.py 2010-03-23 00:39:45 +0000
615+++ lib/lp/registry/browser/productseries.py 2010-04-07 13:24:38 +0000
616@@ -1,4 +1,4 @@
617-# Copyright 2009 Canonical Ltd. This software is licensed under the
618+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
619 # GNU Affero General Public License version 3 (see the file LICENSE).
620
621 """View classes for `IProductSeries`."""
622@@ -20,7 +20,7 @@
623 'ProductSeriesOverviewNavigationMenu',
624 'ProductSeriesRdfView',
625 'ProductSeriesReviewView',
626- 'ProductSeriesSourceListView',
627+ 'ProductSeriesSetBranchView',
628 'ProductSeriesSpecificationsMenu',
629 'ProductSeriesUbuntuPackagingView',
630 'ProductSeriesView',
631@@ -34,6 +34,7 @@
632 from zope.component import getUtility
633 from zope.app.form.browser import TextAreaWidget, TextWidget
634 from zope.formlib import form
635+from zope.interface import Interface
636 from zope.schema import Choice
637 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
638
639@@ -41,7 +42,7 @@
640
641 from canonical.cachedproperty import cachedproperty
642 from canonical.launchpad import _
643-from lp.code.browser.branchref import BranchRef
644+from canonical.launchpad.fields import URIField
645 from lp.blueprints.browser.specificationtarget import (
646 HasSpecificationsMenuMixin)
647 from lp.blueprints.interfaces.specification import (
648@@ -49,9 +50,15 @@
649 from lp.bugs.interfaces.bugtask import BugTaskStatus
650 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
651 from canonical.launchpad.helpers import browserLanguages
652+from lp.code.browser.branch import BranchNameValidationMixin
653+from lp.code.browser.branchref import BranchRef
654+from lp.code.enums import BranchType, RevisionControlSystems
655+from lp.code.interfaces.branch import (
656+ BranchCreationForbidden, BranchExists, IBranch)
657 from lp.code.interfaces.branchjob import IRosettaUploadJobSource
658+from lp.code.interfaces.branchtarget import IBranchTarget
659 from lp.code.interfaces.codeimport import (
660- ICodeImportSet)
661+ ICodeImport, ICodeImportSet)
662 from lp.services.worlddata.interfaces.country import ICountry
663 from lp.bugs.interfaces.bugtask import IBugTaskSet
664 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
665@@ -70,12 +77,13 @@
666 Link, Navigation, NavigationMenu, StandardLaunchpadFacets, stepthrough,
667 stepto)
668 from canonical.launchpad.webapp.authorization import check_permission
669-from canonical.launchpad.webapp.batching import BatchNavigator
670 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
671-from canonical.launchpad.webapp.interfaces import NotFoundError
672+from canonical.launchpad.webapp.interfaces import (
673+ NotFoundError, UnexpectedFormData)
674 from canonical.launchpad.webapp.launchpadform import (
675 action, custom_widget, LaunchpadEditFormView, LaunchpadFormView)
676 from canonical.launchpad.webapp.menu import structured
677+from canonical.widgets.itemswidgets import LaunchpadRadioWidget
678 from canonical.widgets.textwidgets import StrippedTextWidget
679
680 from lp.registry.browser import (
681@@ -83,6 +91,9 @@
682 from lp.registry.interfaces.series import SeriesStatus
683 from lp.registry.interfaces.productseries import IProductSeries
684
685+from lazr.enum import DBItem
686+from lazr.restful.interface import copy_field, use_template
687+
688
689 def quote(text):
690 """Escape and quote text."""
691@@ -644,7 +655,369 @@
692 self.next_url = canonical_url(product)
693
694
695-class ProductSeriesLinkBranchView(LaunchpadEditFormView):
696+LINK_LP_BZR = 'link-lp-bzr'
697+CREATE_NEW = 'create-new'
698+IMPORT_EXTERNAL = 'import-external'
699+
700+
701+BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
702+ SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
703+ _("Link to a Bazaar branch already on Launchpad")),
704+ SimpleTerm(CREATE_NEW, CREATE_NEW,
705+ _("Create a new, empty branch in Launchpad and "
706+ "link to this series")),
707+ SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
708+ _("Import a branch hosted somewhere else")),
709+ ))
710+
711+
712+class RevisionControlSystemsExtended(RevisionControlSystems):
713+ """External RCS plus Bazaar."""
714+ BZR = DBItem(99, """
715+ Bazaar
716+
717+ External Bazaar branch.
718+ """)
719+
720+
721+class SetBranchForm(Interface):
722+ """The fields presented on the form for setting a branch."""
723+
724+ use_template(
725+ ICodeImport,
726+ ['cvs_module'])
727+
728+ rcs_type = Choice(title=_("Type of RCS"),
729+ required=False, vocabulary=RevisionControlSystemsExtended,
730+ description=_(
731+ "The version control system to import from. "))
732+
733+ repo_url = URIField(
734+ title=_("Branch URL"), required=True,
735+ description=_("The URL of the branch."),
736+ allowed_schemes=["http", "https"],
737+ allow_userinfo=False,
738+ allow_port=True,
739+ allow_query=False,
740+ allow_fragment=False,
741+ trailing_slash=False)
742+
743+ branch_location = copy_field(
744+ IProductSeries['branch'],
745+ __name__='branch_location',
746+ title=_('Branch'),
747+ description=_(
748+ "The Bazaar branch for this series in Launchpad, "
749+ "if one exists."),
750+ )
751+
752+ branch_type = Choice(
753+ title=_('Import type'),
754+ vocabulary=BRANCH_TYPE_VOCABULARY,
755+ description=_("The type of import"),
756+ required=True)
757+
758+ branch_name = copy_field(
759+ IBranch['name'],
760+ __name__='branch_name',
761+ title=_('Branch name'),
762+ description=_(''),
763+ required=True,
764+ )
765+
766+ branch_owner = copy_field(
767+ IBranch['owner'],
768+ __name__='branch_owner',
769+ title=_('Branch owner'),
770+ description=_(''),
771+ required=True,
772+ )
773+
774+
775+class ProductSeriesSetBranchView(LaunchpadFormView, ProductSeriesView,
776+ BranchNameValidationMixin):
777+ """The view to set a branch for the ProductSeries."""
778+
779+ schema = SetBranchForm
780+ # Set for_input to True to ensure fields marked read-only will be editable
781+ # upon creation.
782+ for_input = True
783+
784+ custom_widget('rcs_type', LaunchpadRadioWidget)
785+ custom_widget('branch_type', LaunchpadRadioWidget)
786+ initial_values = {
787+ 'rcs_type': RevisionControlSystemsExtended.BZR,
788+ 'branch_type': LINK_LP_BZR,
789+ }
790+
791+ def setUpWidgets(self):
792+ """See `LaunchpadFormView`."""
793+ super(ProductSeriesSetBranchView, self).setUpWidgets()
794+
795+ def render(widget, term_value, current_value, label=None):
796+ term = widget.vocabulary.getTerm(term_value)
797+ if term.value == current_value:
798+ render = widget.renderSelectedItem
799+ else:
800+ render = widget.renderItem
801+ if label is None:
802+ label = term.title
803+ value = term.token
804+ return render(index=term.value,
805+ text=label,
806+ value=value,
807+ name=widget.name,
808+ cssClass='')
809+
810+ widget = self.widgets['rcs_type']
811+ vocab = widget.vocabulary
812+ current_value = widget._getFormValue()
813+ self.rcs_type_cvs = render(widget, vocab.CVS, current_value, 'CVS')
814+ self.rcs_type_svn = render(widget, vocab.BZR_SVN, current_value,
815+ 'SVN')
816+ self.rcs_type_git = render(widget, vocab.GIT, current_value)
817+ self.rcs_type_hg = render(widget, vocab.HG, current_value)
818+ self.rcs_type_bzr = render(widget, vocab.BZR, current_value)
819+ self.rcs_type_emptymarker = widget._emptyMarker()
820+
821+ widget = self.widgets['branch_type']
822+ current_value = widget._getFormValue()
823+ vocab = widget.vocabulary
824+
825+ (self.branch_type_link,
826+ self.branch_type_create,
827+ self.branch_type_import) = [
828+ render(widget, value, current_value)
829+ for value in (LINK_LP_BZR, CREATE_NEW, IMPORT_EXTERNAL)]
830+
831+ def _validateLinkLpBzr(self, data):
832+ """Validate data for link-lp-bzr case."""
833+ if 'branch_location' not in data:
834+ self.setFieldError(
835+ 'branch_location',
836+ 'The branch location must be set.')
837+
838+ def _validateCreateNew(self, data):
839+ """Validate data for create new case."""
840+ self._validateBranch(data)
841+
842+ def _validateImportExternal(self, data):
843+ """Validate data for import external case."""
844+ rcs_type = data.get('rcs_type')
845+ repo_url = data.get('repo_url')
846+
847+ if repo_url is None:
848+ self.setFieldError('repo_url',
849+ 'You must set the external repository URL.')
850+ else:
851+ # Ensure this URL has not been imported before.
852+ code_import = getUtility(ICodeImportSet).getByURL(repo_url)
853+ if code_import is not None:
854+ self.setFieldError(
855+ 'repo_url',
856+ structured("""
857+ This foreign branch URL is already specified for
858+ the imported branch <a href="%s">%s</a>.""",
859+ canonical_url(code_import.branch),
860+ code_import.branch.unique_name))
861+
862+ # RCS type is mandatory.
863+ # This condition should never happen since an initial value is set.
864+ if rcs_type is None:
865+ # The error shows but does not identify the widget.
866+ self.setFieldError(
867+ 'rcs_type',
868+ 'You must specify the type of RCS for the remote host.')
869+ elif rcs_type == RevisionControlSystemsExtended.CVS:
870+ if 'cvs_module' not in data:
871+ self.setFieldError(
872+ 'cvs_module',
873+ 'The CVS module must be set.')
874+ self._validateBranch(data)
875+
876+ def _validateBranch(self, data):
877+ """Validate that branch name and owner are set."""
878+ if 'branch_name' not in data:
879+ self.setFieldError(
880+ 'branch_name',
881+ 'The branch name must be set.')
882+ if 'branch_owner' not in data:
883+ self.setFieldError(
884+ 'branch_owner',
885+ 'The branch owner must be set.')
886+
887+ def _setRequired(self, names, value):
888+ """Mark the widget field as optional."""
889+ for name in names:
890+ widget = self.widgets[name]
891+ # The 'required' property on the widget context is set to False.
892+ # The widget also has a 'required' property but it isn't used
893+ # during validation.
894+ widget.context.required = value
895+
896+ def _validSchemes(self, rcs_type):
897+ """Return the valid schemes for the repository URL."""
898+ schemes = set(['http', 'https'])
899+ # Extend the allowed schemes for the repository URL based on
900+ # rcs_type.
901+ extra_schemes = {
902+ RevisionControlSystemsExtended.BZR_SVN:['svn'],
903+ RevisionControlSystemsExtended.GIT:['git'],
904+ }
905+ schemes.update(extra_schemes.get(rcs_type, []))
906+ return schemes
907+
908+ def validate_widgets(self, data, names=None):
909+ """See `LaunchpadFormView`."""
910+ names = ['branch_type', 'rcs_type']
911+ super(ProductSeriesSetBranchView, self).validate_widgets(data, names)
912+ branch_type = data.get('branch_type')
913+ if branch_type == LINK_LP_BZR:
914+ # Mark other widgets as non-required.
915+ self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
916+ 'branch_name', 'branch_owner'], False)
917+ elif branch_type == CREATE_NEW:
918+ self._setRequired(
919+ ['branch_location', 'repo_url', 'rcs_type', 'cvs_module'],
920+ False)
921+ elif branch_type == IMPORT_EXTERNAL:
922+ rcs_type = data.get('rcs_type')
923+
924+ # Set the valid schemes based on rcs_type.
925+ self.widgets['repo_url'].field.allowed_schemes = (
926+ self._validSchemes(rcs_type))
927+ # The branch location is not required for validation.
928+ self._setRequired(['branch_location'], False)
929+ # The cvs_module is required if it is a CVS import.
930+ if rcs_type == RevisionControlSystemsExtended.CVS:
931+ self._setRequired(['cvs_module'], True)
932+ else:
933+ raise AssertionError("Unknown branch type %s" % branch_type)
934+ # Perform full validation now.
935+ super(ProductSeriesSetBranchView, self).validate_widgets(data)
936+
937+ def validate(self, data):
938+ """See `LaunchpadFormView`."""
939+ # If widget validation returned errors then there is no need to
940+ # continue as we'd likely just override the errors reported there.
941+ if len(self.errors) > 0:
942+ return
943+ branch_type = data['branch_type']
944+ if branch_type == IMPORT_EXTERNAL:
945+ self._validateImportExternal(data)
946+ elif branch_type == LINK_LP_BZR:
947+ self._validateLinkLpBzr(data)
948+ elif branch_type == CREATE_NEW:
949+ self._validateCreateNew(data)
950+ else:
951+ raise AssertionError("Unknown branch type %s" % branch_type)
952+
953+ @property
954+ def target(self):
955+ """The branch target for the context."""
956+ return IBranchTarget(self.context)
957+
958+ @action(_('Update'), name='update')
959+ def update_action(self, action, data):
960+ self.next_url = canonical_url(self.context)
961+ branch_type = data.get('branch_type')
962+ if branch_type == LINK_LP_BZR:
963+ branch_location = data.get('branch_location')
964+ if branch_location != self.context.branch:
965+ self.context.branch = branch_location
966+ # Request an initial upload of translation files.
967+ getUtility(IRosettaUploadJobSource).create(
968+ self.context.branch, NULL_REVISION)
969+ else:
970+ self.context.branch = branch_location
971+ self.request.response.addInfoNotification(
972+ 'Series code location updated.')
973+ else:
974+ branch_name = data.get('branch_name')
975+ branch_owner = data.get('branch_owner')
976+
977+ # Create a new branch.
978+ if branch_type == CREATE_NEW:
979+ branch = self._createBzrBranch(
980+ BranchType.HOSTED, branch_name, branch_owner)
981+ if branch is not None:
982+ self.context.branch = branch
983+ self.request.response.addInfoNotification(
984+ 'New branch created and linked to the series.')
985+
986+ # Import or mirror an external branch.
987+ elif branch_type == IMPORT_EXTERNAL:
988+ # Either create an externally hosted bzr branch
989+ # (a.k.a. 'mirrored') or create a new code import.
990+ rcs_type = data.get('rcs_type')
991+ if rcs_type == RevisionControlSystemsExtended.BZR:
992+ branch = self._createBzrBranch(
993+ BranchType.MIRRORED, branch_name, branch_owner,
994+ data['repo_url'])
995+
996+ if branch is not None:
997+ self.context.branch = branch
998+ self.request.response.addInfoNotification(
999+ 'Mirrored branch created and linked to '
1000+ 'the series.')
1001+ else:
1002+ # We need to create an import request.
1003+
1004+ # Ensure the URL has not already been imported.
1005+ if rcs_type == RevisionControlSystemsExtended.CVS:
1006+ cvs_root = data.get('repo_url')
1007+ cvs_module = data.get('cvs_module')
1008+ url = None
1009+ else:
1010+ cvs_root = None
1011+ cvs_module = None
1012+ url = data.get('repo_url')
1013+ rcs_item = RevisionControlSystems.items[rcs_type.name]
1014+ code_import = getUtility(ICodeImportSet).new(
1015+ registrant=branch_owner,
1016+ target=self.target,
1017+ branch_name=branch_name,
1018+ rcs_type=rcs_item,
1019+ url=url,
1020+ cvs_root=cvs_root,
1021+ cvs_module=cvs_module)
1022+ self.context.branch = code_import.branch
1023+ self.request.response.addInfoNotification(
1024+ 'Code import created and branch linked to the '
1025+ 'series.')
1026+ else:
1027+ raise UnexpectedFormData(branch_type)
1028+
1029+ def _createBzrBranch(self, branch_type, branch_name,
1030+ branch_owner, repo_url=None):
1031+ """Create a new Bazaar branch. It may be hosted or mirrored.
1032+
1033+ Return the branch on success or None.
1034+ """
1035+ branch = None
1036+ try:
1037+ namespace = self.target.getNamespace(branch_owner)
1038+ branch = namespace.createBranch(branch_type=branch_type,
1039+ name=branch_name,
1040+ registrant=self.user,
1041+ url=repo_url)
1042+ if branch_type == BranchType.MIRRORED:
1043+ branch.requestMirror()
1044+ except BranchCreationForbidden:
1045+ self.addError(
1046+ "You are not allowed to create branches in %s." %
1047+ self.context.displayname)
1048+ except BranchExists, e:
1049+ self._setBranchExists(e.existing_branch, 'branch_name')
1050+ return branch
1051+
1052+ @property
1053+ def cancel_url(self):
1054+ """See `LaunchpadFormView`."""
1055+ return canonical_url(self.context)
1056+
1057+
1058+class ProductSeriesLinkBranchView(LaunchpadEditFormView, ProductSeriesView):
1059 """View to set the bazaar branch for a product series."""
1060
1061 schema = IProductSeries
1062@@ -753,23 +1126,6 @@
1063 return encodeddata
1064
1065
1066-class ProductSeriesSourceListView(LaunchpadView):
1067- """A listing of all the running imports.
1068-
1069- See `ICodeImportSet.getActiveImports` for our definition of running.
1070- """
1071-
1072- page_title = 'Available code imports'
1073- label = page_title
1074-
1075- def initialize(self):
1076- """See `LaunchpadFormView`."""
1077- self.text = self.request.get('text')
1078- results = getUtility(ICodeImportSet).getActiveImports(text=self.text)
1079-
1080- self.batchnav = BatchNavigator(results, self.request)
1081-
1082-
1083 class ProductSeriesFileBugRedirect(LaunchpadView):
1084 """Redirect to the product's +filebug page."""
1085
1086
1087=== added file 'lib/lp/registry/browser/tests/productseries-setbranch-view.txt'
1088--- lib/lp/registry/browser/tests/productseries-setbranch-view.txt 1970-01-01 00:00:00 +0000
1089+++ lib/lp/registry/browser/tests/productseries-setbranch-view.txt 2010-04-07 13:24:38 +0000
1090@@ -0,0 +1,339 @@
1091+Set branch
1092+----------
1093+
1094+The productseries +setbranch view allows the user to set a branch for
1095+this series. The branch can be one that already exists in Launchpad,
1096+or a new branch in Launchpad can be defined, or it can be a repository
1097+that exists externally in a variety of version control systems.
1098+
1099+ >>> from canonical.launchpad.testing.pages import find_tag_by_id
1100+ >>> product = factory.makeProduct(name="chevy")
1101+ >>> series = factory.makeProductSeries(name="impala", product=product)
1102+ >>> transaction.commit()
1103+ >>> login_person(product.owner)
1104+ >>> view = create_initialized_view(series, name='+setbranch',
1105+ ... principal=product.owner)
1106+ >>> print find_tag_by_id(view.render(), 'maincontent')
1107+ <div...
1108+ ...Link to a Bazaar branch already on Launchpad...
1109+ ...Create a new, empty branch in Launchpad and link to this series...
1110+ ...Import a branch hosted somewhere else...
1111+ ...Branch name:...
1112+ ...Branch owner:...
1113+
1114+
1115+Linking to an existing branch
1116+-----------------------------
1117+
1118+If linking to an existing branch is selected then the branch location
1119+must be provided.
1120+
1121+ >>> form = {
1122+ ... 'field.branch_type': 'link-lp-bzr',
1123+ ... 'field.actions.update': 'Update',
1124+ ... }
1125+ >>> view = create_initialized_view(series, name='+setbranch',
1126+ ... principal=product.owner, form=form)
1127+ >>> for error in view.errors:
1128+ ... print error
1129+ The branch location must be set.
1130+
1131+Setting the branch location to an invalid branch results in another
1132+validation error.
1133+
1134+ >>> form = {
1135+ ... 'field.branch_type': 'link-lp-bzr',
1136+ ... 'field.branch_location': 'foo',
1137+ ... 'field.actions.update': 'Update',
1138+ ... }
1139+ >>> view = create_initialized_view(series, name='+setbranch',
1140+ ... principal=product.owner, form=form)
1141+ >>> for error in view.errors:
1142+ ... print error
1143+ ('Invalid value', InvalidValue("token 'foo' not found in vocabulary"))
1144+
1145+Providing a valid branch results in a successful linking.
1146+
1147+ >>> series.branch is None
1148+ True
1149+ >>> branch = factory.makeBranch(name='impala-branch',
1150+ ... owner=product.owner, product=product)
1151+ >>> form = {
1152+ ... 'field.branch_type': 'link-lp-bzr',
1153+ ... 'field.branch_location': branch.unique_name,
1154+ ... 'field.actions.update': 'Update',
1155+ ... }
1156+ >>> view = create_initialized_view(series, name='+setbranch',
1157+ ... principal=product.owner, form=form)
1158+ >>> for error in view.errors:
1159+ ... print error
1160+ >>> for notification in view.request.response.notifications:
1161+ ... print notification.message
1162+ Series code location updated.
1163+
1164+ >>> print series.branch.name
1165+ impala-branch
1166+
1167+
1168+Creating a new branch
1169+---------------------
1170+
1171+When creating a new branch the branch name and owner must be specified.
1172+
1173+ >>> series = factory.makeProductSeries(name="camaro", product=product)
1174+ >>> transaction.commit()
1175+
1176+ >>> form = {
1177+ ... 'field.branch_type': 'create-new',
1178+ ... 'field.actions.update': 'Update',
1179+ ... }
1180+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1181+ >>> for notification in view.request.response.notifications:
1182+ ... print notification.message
1183+ >>> for error in view.errors:
1184+ ... print error
1185+ The branch name must be set.
1186+ The branch owner must be set.
1187+
1188+ >>> from lp.registry.interfaces.person import IPersonSet
1189+ >>> mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1190+ >>> form = {
1191+ ... 'field.branch_type': 'create-new',
1192+ ... 'field.branch_name': 'camaro-branch',
1193+ ... 'field.branch_owner': product.owner.name,
1194+ ... 'field.actions.update': 'Update',
1195+ ... }
1196+
1197+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1198+ >>> for error in view.errors:
1199+ ... print error
1200+ >>> for notification in view.request.response.notifications:
1201+ ... print notification.message
1202+ New branch created and linked to the series.
1203+ >>> print series.branch.name
1204+ camaro-branch
1205+
1206+
1207+Import a branch hosted elsewhere
1208+--------------------------------
1209+
1210+Importing an externally hosted branch can either be a mirror, if a
1211+Bazaar branch, or an import, if a git, hg, cvs, or svn branch.
1212+
1213+Lots of data are required to create an import.
1214+
1215+ >>> series = factory.makeProductSeries(name="blazer", product=product)
1216+ >>> transaction.commit()
1217+
1218+ >>> form = {
1219+ ... 'field.branch_type': 'import-external',
1220+ ... 'field.actions.update': 'Update',
1221+ ... }
1222+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1223+ >>> for notification in view.request.response.notifications:
1224+ ... print notification.message
1225+ >>> for error in view.errors:
1226+ ... print error
1227+ You must set the external repository URL.
1228+ You must specify the type of RCS for the remote host.
1229+ The branch name must be set.
1230+ The branch owner must be set.
1231+
1232+For Bazaar branches the scheme may only be http or https.
1233+
1234+ >>> form = {
1235+ ... 'field.branch_type': 'import-external',
1236+ ... 'field.rcs_type': 'BZR',
1237+ ... 'field.branch_name': 'blazer-branch',
1238+ ... 'field.branch_owner': product.owner.name,
1239+ ... 'field.repo_url': 'bzr://bzr.com/foo',
1240+ ... 'field.actions.update': 'Update',
1241+ ... }
1242+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1243+ >>> for notification in view.request.response.notifications:
1244+ ... print notification.message
1245+ >>> for error in view.errors:
1246+ ... print error
1247+ ('repo_url'...The URI scheme "bzr" is not allowed. Only URIs with the following schemes may be
1248+ used: http, https'))
1249+
1250+A correct URL is accepted.
1251+
1252+ >>> form = {
1253+ ... 'field.branch_type': 'import-external',
1254+ ... 'field.rcs_type': 'BZR',
1255+ ... 'field.branch_name': 'blazer-branch',
1256+ ... 'field.branch_owner': product.owner.name,
1257+ ... 'field.repo_url': 'http://bzr.com/foo',
1258+ ... 'field.actions.update': 'Update',
1259+ ... }
1260+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1261+ >>> for error in view.errors:
1262+ ... print error
1263+ >>> for notification in view.request.response.notifications:
1264+ ... print notification.message
1265+ Mirrored branch created and linked to the series.
1266+ >>> print series.branch.name
1267+ blazer-branch
1268+
1269+Git branches cannnot use svn.
1270+
1271+ >>> form = {
1272+ ... 'field.branch_type': 'import-external',
1273+ ... 'field.rcs_type': 'GIT',
1274+ ... 'field.branch_name': 'chevette-branch',
1275+ ... 'field.branch_owner': product.owner.name,
1276+ ... 'field.repo_url': 'svn://svn.com/chevette',
1277+ ... 'field.actions.update': 'Update',
1278+ ... }
1279+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1280+ >>> for notification in view.request.response.notifications:
1281+ ... print notification.message
1282+ >>> for error in view.errors:
1283+ ... print error
1284+ ('repo_url'...'The URI scheme "svn" is not allowed. Only
1285+ URIs with the following schemes may be used: git, http, https'))
1286+
1287+But Git branches may use git.
1288+
1289+ >>> series = factory.makeProductSeries(name="chevette", product=product)
1290+ >>> transaction.commit()
1291+ >>> form = {
1292+ ... 'field.branch_type': 'import-external',
1293+ ... 'field.rcs_type': 'GIT',
1294+ ... 'field.branch_name': 'chevette-branch',
1295+ ... 'field.branch_owner': product.owner.name,
1296+ ... 'field.repo_url': 'git://github.com/chevette',
1297+ ... 'field.actions.update': 'Update',
1298+ ... }
1299+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1300+ >>> transaction.commit()
1301+ >>> for error in view.errors:
1302+ ... print error
1303+ >>> for notification in view.request.response.notifications:
1304+ ... print notification.message
1305+ Code import created and branch linked to the series.
1306+ >>> print series.branch.name
1307+ chevette-branch
1308+
1309+But Subversion branches cannnot use git.
1310+
1311+ >>> form = {
1312+ ... 'field.branch_type': 'import-external',
1313+ ... 'field.rcs_type': 'BZR_SVN',
1314+ ... 'field.branch_name': 'suburban-branch',
1315+ ... 'field.branch_owner': product.owner.name,
1316+ ... 'field.repo_url': 'git://github.com/suburban',
1317+ ... 'field.actions.update': 'Update',
1318+ ... }
1319+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1320+ >>> for notification in view.request.response.notifications:
1321+ ... print notification.message
1322+ >>> for error in view.errors:
1323+ ... print error
1324+ ('repo_url'...'The URI scheme "git" is not allowed. Only
1325+ URIs with the following schemes may be used: http, https, svn'))
1326+
1327+But Subversion branches may use svn as the scheme.
1328+
1329+ >>> series = factory.makeProductSeries(name="suburban", product=product)
1330+ >>> transaction.commit()
1331+ >>> form = {
1332+ ... 'field.branch_type': 'import-external',
1333+ ... 'field.rcs_type': 'BZR_SVN',
1334+ ... 'field.branch_name': 'suburban-branch',
1335+ ... 'field.branch_owner': product.owner.name,
1336+ ... 'field.repo_url': 'svn://svn.com/suburban',
1337+ ... 'field.actions.update': 'Update',
1338+ ... }
1339+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1340+ >>> for error in view.errors:
1341+ ... print error
1342+ >>> for notification in view.request.response.notifications:
1343+ ... print notification.message
1344+ Code import created and branch linked to the series.
1345+ >>> print series.branch.name
1346+ suburban-branch
1347+
1348+Mercurial branches must use http or https as the scheme.
1349+
1350+ >>> series = factory.makeProductSeries(name="malibu", product=product)
1351+ >>> transaction.commit()
1352+ >>> form = {
1353+ ... 'field.branch_type': 'import-external',
1354+ ... 'field.rcs_type': 'HG',
1355+ ... 'field.branch_name': 'malibu-branch',
1356+ ... 'field.branch_owner': product.owner.name,
1357+ ... 'field.repo_url': 'https://mercurial.com/branch',
1358+ ... 'field.actions.update': 'Update',
1359+ ... }
1360+ >>> view = create_initialized_view(series, name='+setbranch', principal=product.owner, form=form)
1361+ >>> for error in view.errors:
1362+ ... print error
1363+ >>> for notification in view.request.response.notifications:
1364+ ... print notification.message
1365+ Code import created and branch linked to the series.
1366+ >>> print series.branch.name
1367+ malibu-branch
1368+
1369+CVS branches must use http or https as the scheme and must have the
1370+CVS module field specified.
1371+
1372+ >>> series = factory.makeProductSeries(name="corvair", product=product)
1373+ >>> transaction.commit()
1374+ >>> form = {
1375+ ... 'field.branch_type': 'import-external',
1376+ ... 'field.rcs_type': 'CVS',
1377+ ... 'field.branch_name': 'corvair-branch',
1378+ ... 'field.branch_owner': product.owner.name,
1379+ ... 'field.repo_url': 'https://cvs.com/branch',
1380+ ... 'field.actions.update': 'Update',
1381+ ... }
1382+ >>> view = create_initialized_view(series, name='+setbranch',
1383+ ... principal=product.owner, form=form)
1384+ >>> for notification in view.request.response.notifications:
1385+ ... print notification.message
1386+ >>> for error in view.errors:
1387+ ... print error
1388+ The CVS module must be set.
1389+
1390+ >>> form = {
1391+ ... 'field.branch_type': 'import-external',
1392+ ... 'field.rcs_type': 'CVS',
1393+ ... 'field.branch_name': 'corvair-branch',
1394+ ... 'field.branch_owner': product.owner.name,
1395+ ... 'field.repo_url': 'https://cvs.com/branch',
1396+ ... 'field.cvs_module': 'root',
1397+ ... 'field.actions.update': 'Update',
1398+ ... }
1399+ >>> view = create_initialized_view(series, name='+setbranch',
1400+ ... principal=product.owner, form=form)
1401+ >>> for error in view.errors:
1402+ ... print error
1403+ >>> for notification in view.request.response.notifications:
1404+ ... print notification.message
1405+ Code import created and branch linked to the series.
1406+ >>> print series.branch.name
1407+ corvair-branch
1408+
1409+Attempting to import a location that has already been imported results
1410+in an error.
1411+
1412+ >>> form = {
1413+ ... 'field.branch_type': 'import-external',
1414+ ... 'field.rcs_type': 'GIT',
1415+ ... 'field.branch_name': 'chevette-branch-dup',
1416+ ... 'field.branch_owner': product.owner.name,
1417+ ... 'field.repo_url': 'git://github.com/chevette',
1418+ ... 'field.actions.update': 'Update',
1419+ ... }
1420+ >>> view = create_initialized_view(series, name='+setbranch',
1421+ ... principal=product.owner, form=form)
1422+ >>> for error in view.errors:
1423+ ... print error
1424+ <BLANKLINE>
1425+ This foreign branch URL is already specified for
1426+ the imported branch <a href="http://code.launchpad.dev/~.../chevy/chevette-branch">~.../chevy/chevette-branch</a>.
1427+
1428+ >>> for notification in view.request.response.notifications:
1429+ ... print notification.message
1430
1431=== added file 'lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt'
1432--- lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt 1970-01-01 00:00:00 +0000
1433+++ lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt 2010-04-07 13:24:38 +0000
1434@@ -0,0 +1,147 @@
1435+Setting the branch for a product series
1436+=======================================
1437+
1438+A product series should have a branch set for it. The branch can be
1439+hosted on Launchpad or somewhere else. Foreign branches can be in
1440+Bazaar, Git, Mercurial, Subversion, or CVS. Though internally
1441+Launchpad treats those scenarios differently we provide a single page
1442+to the user to set up the branch.
1443+
1444+At present, the unified page for setting up the branch is not linked
1445+from anywhere, so it must be navigated to directly.
1446+
1447+ >>> browser = setupBrowser(auth="Basic test@canonical.com:test")
1448+ >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
1449+
1450+The default choice for the type of branch to set is one that
1451+already exists on Launchpad.
1452+
1453+ >>> print_radio_button_field(browser.contents, 'branch_type')
1454+ (*) Link to a Bazaar branch already on Launchpad
1455+ ( ) Create a new, empty branch in Launchpad and link to this series
1456+ ( ) Import a branch hosted somewhere else
1457+
1458+
1459+Linking to an existing branch
1460+-----------------------------
1461+
1462+A user can choose to link to an existing branch on Launchpad.
1463+
1464+ >>> login('test@canonical.com')
1465+ >>> from zope.component import getUtility
1466+ >>> from lp.registry.interfaces.product import IProductSet
1467+ >>> productset = getUtility(IProductSet)
1468+ >>> firefox = productset.getByName('firefox')
1469+ >>> branch = factory.makeBranch(name="firefox-hosted-branch", product=firefox)
1470+ >>> branch_name = branch.unique_name
1471+ >>> logout()
1472+
1473+ >>> browser.getControl(name='field.branch_location').value = branch_name
1474+ >>> browser.getControl('Update').click()
1475+ >>> for message in get_feedback_messages(browser.contents):
1476+ ... print extract_text(message)
1477+ Series code location updated.
1478+ >>> print browser.url
1479+ http://launchpad.dev/firefox/trunk
1480+
1481+
1482+Creating a new branch
1483+---------------------
1484+
1485+A brand new, empty branch on Launchpad can be created and set as the
1486+series branch.
1487+
1488+ >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
1489+ >>> browser.getControl('Create a new, empty branch in Launchpad').click()
1490+ >>> browser.getControl('Update').click()
1491+ >>> for message in get_feedback_messages(browser.contents):
1492+ ... print extract_text(message)
1493+ There is 1 error.
1494+ Required input is missing.
1495+
1496+However in order to create the branch the name and owner must be
1497+specified. The owner is a pre-populated dropdown list so the default
1498+can be used.
1499+
1500+ >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
1501+ >>> browser.getControl('Create a new, empty branch in Launchpad').click()
1502+ >>> browser.getControl(name='field.branch_name').value = 'new-firefox-branch'
1503+ >>> browser.getControl('Update').click()
1504+ >>> for message in get_feedback_messages(browser.contents):
1505+ ... print extract_text(message)
1506+ New branch created and linked to the series.
1507+ >>> print browser.url
1508+ http://launchpad.dev/firefox/trunk
1509+
1510+
1511+Linking to an external branch
1512+-----------------------------
1513+
1514+An external branch can be linked. The branch can be a Bazaar branch
1515+or be a Git, Mercurial, Subversion, or CVS branch.
1516+
1517+Each of these types must provide the URL of the external repository,
1518+the branch name to use in Launchpad, and the branch owner.
1519+
1520+ >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
1521+ >>> browser.getControl('Import a branch hosted somewhere else').click()
1522+ >>> browser.getControl('Branch name').value = 'bzr-firefox-branch'
1523+ >>> browser.getControl('Bazaar', index=0).click()
1524+ >>> browser.getControl('Branch URL').value = 'https://bzr.example.com/branch'
1525+ >>> browser.getControl('Update').click()
1526+ >>> for message in get_feedback_messages(browser.contents):
1527+ ... print extract_text(message)
1528+ Series code location updated.
1529+ >>> print browser.url
1530+ http://launchpad.dev/firefox/trunk
1531+
1532+The process is the same for a Git external branch, though the novel
1533+"git://" scheme can also be used.
1534+
1535+ >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
1536+ >>> browser.getControl('Import a branch hosted somewhere else').click()
1537+ >>> browser.getControl('Branch name').value = 'git-firefox-branch'
1538+ >>> browser.getControl('Git').click()
1539+ >>> browser.getControl('Branch URL').value = 'git://git.example.com/branch'
1540+ >>> browser.getControl('Update').click()
1541+ >>> for message in get_feedback_messages(browser.contents):
1542+ ... print extract_text(message)
1543+ Code import created and branch linked to the series.
1544+ >>> print browser.url
1545+ http://launchpad.dev/firefox/trunk
1546+
1547+Likewise Subversion can use the "svn://" scheme.
1548+
1549+ >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
1550+ >>> browser.getControl('Import a branch hosted somewhere else').click()
1551+ >>> browser.getControl('Branch name').value = 'svn-firefox-branch'
1552+ >>> browser.getControl('SVN').click()
1553+ >>> browser.getControl('Branch URL').value = 'svn://svn.example.com/branch'
1554+ >>> browser.getControl('Update').click()
1555+ >>> for message in get_feedback_messages(browser.contents):
1556+ ... print extract_text(message)
1557+ Code import created and branch linked to the series.
1558+ >>> print browser.url
1559+ http://launchpad.dev/firefox/trunk
1560+
1561+The branch owner can be the logged in user or one of her teams.
1562+
1563+ >>> browser.open('http://launchpad.dev/firefox/trunk/+setbranch')
1564+ >>> browser.getControl('Import a branch hosted somewhere else').click()
1565+ >>> browser.getControl('Branch name').value = 'hg-firefox-branch'
1566+ >>> browser.getControl('Mercurial').click()
1567+ >>> browser.getControl('Branch URL').value = 'http://hg.example.com/branch'
1568+ >>> browser.getControl('Branch owner').value = ['hwdb-team']
1569+ >>> browser.getControl('Update').click()
1570+ >>> for message in get_feedback_messages(browser.contents):
1571+ ... print extract_text(message)
1572+ Code import created and branch linked to the series.
1573+ >>> print browser.url
1574+ http://launchpad.dev/firefox/trunk
1575+ >>> login('test@canonical.com')
1576+ >>> firefox_trunk = firefox.getSeries('trunk')
1577+ >>> print firefox_trunk.branch.unique_name
1578+ ~hwdb-team/firefox/hg-firefox-branch
1579+ >>> print firefox_trunk.branch.owner.name
1580+ hwdb-team
1581+ >>> logout()
1582
1583=== modified file 'lib/lp/registry/templates/productseries-codesummary.pt'
1584--- lib/lp/registry/templates/productseries-codesummary.pt 2010-03-09 22:06:12 +0000
1585+++ lib/lp/registry/templates/productseries-codesummary.pt 2010-04-07 13:24:38 +0000
1586@@ -29,7 +29,7 @@
1587 <li>
1588 <p>
1589 If the code is in a Bazaar branch not yet on Launchpad
1590- you can either
1591+ you can either:
1592 </p>
1593
1594 <ul class="bulleted" style="margin-bottom: 0;">
1595@@ -39,7 +39,7 @@
1596 registering a mirrored branch</a>
1597 </li>
1598 <li id="ssh-key-directions">
1599- Push the branch directly to Launchpad. eg. with <br />
1600+ Push the branch directly to Launchpad, e.g. with:<br />
1601 <tt><strong>
1602 bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk
1603 </strong></tt>
1604@@ -58,7 +58,7 @@
1605 <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>.
1606 </li>
1607 </ul>
1608-
1609+
1610 <ul class="horizontal">
1611 <li>
1612 <a tal:replace="structure context/menu:overview/link_branch/fmt:link" />
1613
1614=== modified file 'lib/lp/registry/templates/productseries-linkbranch.pt'
1615--- lib/lp/registry/templates/productseries-linkbranch.pt 2009-08-11 21:26:30 +0000
1616+++ lib/lp/registry/templates/productseries-linkbranch.pt 2010-04-07 13:24:38 +0000
1617@@ -7,8 +7,44 @@
1618 i18n:domain="launchpad">
1619 <body>
1620 <div metal:fill-slot="main">
1621- <div metal:use-macro="context/@@launchpad_form/form" />
1622+ <ul>
1623+ <li>If the code is already in a Bazaar branch registered with Launchpad,
1624+ specify it here:
1625+ <div metal:use-macro="context/@@launchpad_form/form" />
1626+ </li>
1627+
1628+ <li>
1629+ <p>
1630+ Otherwise, if the code is in a Bazaar branch not yet on Launchpad
1631+ you can either:
1632+ </p>
1633+
1634+ <ul class="bulleted" style="margin-bottom: 0;">
1635+ <li>
1636+ Have the branch mirrored from a remote location by
1637+ <a tal:attributes="href context/menu:overview/branch_add/fmt:url">
1638+ registering a mirrored branch</a>
1639+ </li>
1640+ <li id="ssh-key-directions">
1641+ Push the branch directly to Launchpad, e.g. with:<br />
1642+ <tt><strong>
1643+ bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/trunk
1644+ </strong></tt>
1645+ <tal:no-keys condition="not:view/user/sshkeys">
1646+ <br/>To authenticate with the Launchpad branch upload service,
1647+ you need to
1648+ <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
1649+ register a SSH key</a>.
1650+ </tal:no-keys>
1651+ </li>
1652+ </ul>
1653+ </li>
1654+
1655+ <li>
1656+ If the code is in git, CVS or Subversion you can
1657+ <a tal:attributes="href view/request_import_link">request that the branch be imported to Bazaar</a>.
1658+ </li>
1659+ </ul>
1660 </div>
1661 </body>
1662 </html>
1663-
1664
1665=== added file 'lib/lp/registry/templates/productseries-setbranch.pt'
1666--- lib/lp/registry/templates/productseries-setbranch.pt 1970-01-01 00:00:00 +0000
1667+++ lib/lp/registry/templates/productseries-setbranch.pt 2010-04-07 13:24:38 +0000
1668@@ -0,0 +1,129 @@
1669+<html
1670+ xmlns="http://www.w3.org/1999/xhtml"
1671+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1672+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1673+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1674+ metal:use-macro="view/macro:page/main_only"
1675+ i18n:domain="launchpad">
1676+
1677+<body>
1678+
1679+<metal:block fill-slot="head_epilogue">
1680+ <style type="text/css">
1681+ .subordinate {
1682+ margin: 0.5em 0 0.5em 4em;
1683+ }
1684+ </style>
1685+</metal:block>
1686+
1687+<div metal:fill-slot="main">
1688+
1689+ <div metal:use-macro="context/@@launchpad_form/form">
1690+
1691+ <metal:formbody fill-slot="widgets">
1692+
1693+ <table class="form">
1694+
1695+ <tr>
1696+ <td>
1697+ <label tal:replace="structure view/branch_type_link">
1698+ Link to a Bazaar branch already in Launchpad
1699+ </label>
1700+ <table class="subordinate">
1701+ <tal:widget define="widget nocall:view/widgets/branch_location">
1702+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
1703+ </tal:widget>
1704+ </table>
1705+ </td>
1706+ </tr>
1707+
1708+ <tr>
1709+ <td>
1710+ <label tal:replace="structure view/branch_type_create">
1711+ Create a new, empty branch in Launchpad and link
1712+ to this series
1713+ </label>
1714+ </td>
1715+ </tr>
1716+
1717+ <tr>
1718+ <td>
1719+ <label tal:replace="structure view/branch_type_import">
1720+ Import a branch hosted somewhere else
1721+ </label>
1722+ <table class="subordinate">
1723+ <tal:widget define="widget nocall:view/widgets/repo_url">
1724+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
1725+ </tal:widget>
1726+
1727+ <tr>
1728+ <td>
1729+ <label tal:replace="structure view/rcs_type_bzr">
1730+ Bazaar, hosted externally
1731+ </label>
1732+ </td>
1733+ </tr>
1734+
1735+ <tr>
1736+ <td>
1737+ <label tal:replace="structure view/rcs_type_git">
1738+ Git
1739+ </label>
1740+ </td>
1741+ </tr>
1742+
1743+ <tr>
1744+ <td>
1745+ <label tal:replace="structure view/rcs_type_svn">
1746+ SVN
1747+ </label>
1748+ </td>
1749+ </tr>
1750+
1751+ <tr>
1752+ <td>
1753+ <label tal:replace="structure view/rcs_type_hg">
1754+ Mercurial
1755+ </label>
1756+ </td>
1757+ </tr>
1758+
1759+ <tr>
1760+ <td>
1761+ <label tal:replace="structure view/rcs_type_cvs">
1762+ CVS
1763+ </label>
1764+ <table class="subordinate">
1765+ <tal:widget define="widget nocall:view/widgets/cvs_module">
1766+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
1767+ </tal:widget>
1768+ </table>
1769+ </td>
1770+ </tr>
1771+
1772+ </table>
1773+ </td>
1774+ </tr>
1775+
1776+ <tal:widget define="widget nocall:view/widgets/branch_name">
1777+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
1778+ </tal:widget>
1779+ <tal:widget define="widget nocall:view/widgets/branch_owner">
1780+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
1781+ </tal:widget>
1782+
1783+ </table>
1784+ <input tal:replace="structure view/rcs_type_emptymarker" />
1785+
1786+ </metal:formbody>
1787+ </div>
1788+
1789+ <script type="text/javascript">
1790+ YUI().use('lp.code.productseries_setbranch', function(Y) {
1791+ Y.on('domready', Y.lp.code.productseries_setbranch.setup);
1792+ });
1793+ </script>
1794+
1795+</div>
1796+</body>
1797+</html>
1798
1799=== modified file 'lib/lp/testing/factory.py'
1800--- lib/lp/testing/factory.py 2010-04-05 17:40:35 +0000
1801+++ lib/lp/testing/factory.py 2010-04-07 13:24:38 +0000
1802@@ -149,8 +149,8 @@
1803
1804 DIFF = """\
1805 === zbqvsvrq svyr 'yvo/yc/pbqr/vagresnprf/qvss.cl'
1806---- yvo/yc/pbqr/vagresnprf/qvss.cl 2009-10-01 13:25:12 +0000
1807-+++ yvo/yc/pbqr/vagresnprf/qvss.cl 2010-02-02 15:48:56 +0000
1808+--- yvo/yc/pbqr/vagresnprf/qvss.cl 2009-10-01 13:25:12 +0000
1809++++ yvo/yc/pbqr/vagresnprf/qvss.cl 2010-02-02 15:48:56 +0000
1810 @@ -121,6 +121,10 @@
1811 'Gur pbasyvpgf grkg qrfpevovat nal cngu be grkg pbasyvpgf.'),
1812 ernqbayl=Gehr))
1813@@ -635,7 +635,7 @@
1814 def makeProcessorFamily(self, name, title=None, description=None,
1815 restricted=False):
1816 """Create a new processor family.
1817-
1818+
1819 :param name: Name of the family (e.g. x86)
1820 :param title: Optional title of the family
1821 :param description: Optional extended description
1822@@ -1568,7 +1568,7 @@
1823
1824 :param branch: If supplied, the branch to set as
1825 ProductSeries.branch.
1826- :param product: If supplied, the name of the series.
1827+ :param name: If supplied, the name of the series.
1828 :param product: If supplied, the series is created for this product.
1829 Otherwise, a new product is created.
1830 """

Subscribers

People subscribed via source and target branches

to status/vote changes: