Merge lp:~blr/launchpad/ui-project-setbranch into lp:launchpad

Proposed by Kit Randel on 2015-05-14
Status: Superseded
Proposed branch: lp:~blr/launchpad/ui-project-setbranch
Merge into: lp:launchpad
Diff against target: 2209 lines (+1213/-478)
27 files modified
LICENSE (+276/-0)
lib/canonical/launchpad/icing/inline-sprites-1.css.in (+5/-1)
lib/canonical/launchpad/icing/style.css (+37/-4)
lib/lp/code/browser/branchlisting.py (+5/-6)
lib/lp/code/browser/configure.zcml (+28/-1)
lib/lp/code/javascript/productseries-setbranch.js (+41/-0)
lib/lp/code/model/gitrepository.py (+2/-2)
lib/lp/code/model/tests/test_gitrepository.py (+11/-0)
lib/lp/code/stories/codeimport/xx-create-codeimport.txt (+2/-1)
lib/lp/code/templates/configure-code-macros.pt (+39/-0)
lib/lp/code/templates/configure-code.pt (+165/-0)
lib/lp/code/templates/product-branch-summary.pt (+1/-2)
lib/lp/code/vocabularies/configure.zcml (+11/-0)
lib/lp/code/vocabularies/gitrepository.py (+19/-0)
lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py (+60/-1)
lib/lp/registry/browser/configure.zcml (+0/-7)
lib/lp/registry/browser/product.py (+414/-8)
lib/lp/registry/browser/productseries.py (+12/-315)
lib/lp/registry/browser/tests/productseries-setbranch-view.txt (+2/-2)
lib/lp/registry/browser/tests/test_product.py (+7/-0)
lib/lp/registry/browser/tests/test_product_views.py (+36/-0)
lib/lp/registry/interfaces/product.py (+8/-0)
lib/lp/registry/model/product.py (+15/-0)
lib/lp/registry/stories/product/xx-product-development-focus.txt (+4/-3)
lib/lp/registry/templates/product-index.pt (+5/-0)
lib/lp/registry/templates/productseries-setbranch.pt (+0/-125)
lib/lp/registry/tests/test_product.py (+8/-0)
To merge this branch: bzr merge lp:~blr/launchpad/ui-project-setbranch
Reviewer Review Type Date Requested Status
William Grant code 2015-05-14 Approve on 2015-06-24
Review via email: mp+259069@code.launchpad.net

This proposal has been superseded by a proposal from 2015-06-25.

Commit Message

Add Product.ProductSetBranchView and UI support for setting product.vcs.

Description of the Change

Provides a new SetBranch view for projects, additionally allowing a default version control system to be defined.

A project's vcs will also be displayed on the product index under Project Information. If not default vcs has been provided, the vcs type is inferred from existing bzr or git branches, with git taking precedence.

Fixes bug in setTargetDefault where an exception was thrown when attempting to set the same targetdefault again.

To post a comment you must log in.
William Grant (wgrant) wrote :

Various code/style comments inline, UX design comments here:

In almost all cases the user will care about at most one VCS. Everything relating to the other VCS is just noise unless that VCS is selected (or a migration is in progress, or they're doing something weird). So the page should start by asking for the project's VCS (not a "default VCS" in the UI), defaulting to Bazaar until our Git support is more complete.

Once you've selected a VCS (not a "default VCS" in the UI) there are three options: push a new repo, choose an existing repo, or mirror a repo from elsewhere. The other VCS's config has to be available too, but it needn't be visible without an extra click.

The "push a new repo" case is handled by the push instructions. But they can be simpler, because the project owner can always create defaults if they don't exist: the Git URL should be git+ssh://user@host/project, and the Bazaar URL lp:project. The initial push instructions will also work fine to write to LP-hosted Bazaar branches or Git repositories, but they shouldn't be shown if the series Bazaar branch is already set and is an import, since those can't be written to directly.

The "choose an existing repo" case requires a selector widget, which can currently just be a textbox to specify a branch or repo path. The Bazaar case should stay as it is now, setting product.development_focus.branch, and the Git case should use setDefaultRepository.

The "mirror a repo from elsewhere" case can currently only exist for Bazaar, as we don't have Git mirroring yet. We can work out how that looks for Git when we get there.

review: Needs Fixing (code)
William Grant (wgrant) :
review: Needs Fixing (code)
William Grant (wgrant) :
review: Needs Fixing (code)
William Grant (wgrant) wrote :

Just one last thing: when I suggested that is_series didn't need to be a property, I meant that it could be a plain attribute. Both implements return a constant value.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'LICENSE'
2--- LICENSE 2015-03-24 13:36:23 +0000
3+++ LICENSE 2015-06-25 03:34:41 +0000
4@@ -13,6 +13,8 @@
5 The Launchpad name and logo are trademarks of Canonical, and may not
6 be used without the prior written permission of Canonical.
7
8+Git SCM logos are licensed Creative Commons Attribution 3.0 Unported.
9+
10 Third-party copyright in this distribution is noted where applicable.
11
12 All rights not expressly granted are reserved.
13@@ -683,3 +685,277 @@
14 <http://www.gnu.org/licenses/>.
15
16 =========================================================================
17+
18+
19+Creative Commons Attribution 3.0 Unported License
20+
21+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
22+CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
23+PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
24+WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
25+PROHIBITED.
26+
27+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
28+AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
29+LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
30+THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH
31+TERMS AND CONDITIONS.
32+
33+1. Definitions
34+
35+"Adaptation" means a work based upon the Work, or upon the Work and
36+other pre-existing works, such as a translation, adaptation,
37+derivative work, arrangement of music or other alterations of a
38+literary or artistic work, or phonogram or performance and includes
39+cinematographic adaptations or any other form in which the Work may be
40+recast, transformed, or adapted including in any form recognizably
41+derived from the original, except that a work that constitutes a
42+Collection will not be considered an Adaptation for the purpose of
43+this License. For the avoidance of doubt, where the Work is a musical
44+work, performance or phonogram, the synchronization of the Work in
45+timed-relation with a moving image ("synching") will be considered an
46+Adaptation for the purpose of this License. "Collection" means a
47+collection of literary or artistic works, such as encyclopedias and
48+anthologies, or performances, phonograms or broadcasts, or other works
49+or subject matter other than works listed in Section 1(f) below,
50+which, by reason of the selection and arrangement of their contents,
51+constitute intellectual creations, in which the Work is included in
52+its entirety in unmodified form along with one or more other
53+contributions, each constituting separate and independent works in
54+themselves, which together are assembled into a collective whole. A
55+work that constitutes a Collection will not be considered an
56+Adaptation (as defined above) for the purposes of this License.
57+"Distribute" means to make available to the public the original and
58+copies of the Work or Adaptation, as appropriate, through sale or
59+other transfer of ownership. "Licensor" means the individual,
60+individuals, entity or entities that offer(s) the Work under the terms
61+of this License. "Original Author" means, in the case of a literary or
62+artistic work, the individual, individuals, entity or entities who
63+created the Work or if no individual or entity can be identified, the
64+publisher; and in addition (i) in the case of a performance the
65+actors, singers, musicians, dancers, and other persons who act, sing,
66+deliver, declaim, play in, interpret or otherwise perform literary or
67+artistic works or expressions of folklore; (ii) in the case of a
68+phonogram the producer being the person or legal entity who first
69+fixes the sounds of a performance or other sounds; and, (iii) in the
70+case of broadcasts, the organization that transmits the broadcast.
71+"Work" means the literary and/or artistic work offered under the terms
72+of this License including without limitation any production in the
73+literary, scientific and artistic domain, whatever may be the mode or
74+form of its expression including digital form, such as a book,
75+pamphlet and other writing; a lecture, address, sermon or other work
76+of the same nature; a dramatic or dramatico-musical work; a
77+choreographic work or entertainment in dumb show; a musical
78+composition with or without words; a cinematographic work to which are
79+assimilated works expressed by a process analogous to cinematography;
80+a work of drawing, painting, architecture, sculpture, engraving or
81+lithography; a photographic work to which are assimilated works
82+expressed by a process analogous to photography; a work of applied
83+art; an illustration, map, plan, sketch or three-dimensional work
84+relative to geography, topography, architecture or science; a
85+performance; a broadcast; a phonogram; a compilation of data to the
86+extent it is protected as a copyrightable work; or a work performed by
87+a variety or circus performer to the extent it is not otherwise
88+considered a literary or artistic work. "You" means an individual or
89+entity exercising rights under this License who has not previously
90+violated the terms of this License with respect to the Work, or who
91+has received express permission from the Licensor to exercise rights
92+under this License despite a previous violation. "Publicly Perform"
93+means to perform public recitations of the Work and to communicate to
94+the public those public recitations, by any means or process,
95+including by wire or wireless means or public digital performances; to
96+make available to the public Works in such a way that members of the
97+public may access these Works from a place and at a place individually
98+chosen by them; to perform the Work to the public by any means or
99+process and the communication to the public of the performances of the
100+Work, including by public digital performance; to broadcast and
101+rebroadcast the Work by any means including signs, sounds or images.
102+"Reproduce" means to make copies of the Work by any means including
103+without limitation by sound or visual recordings and the right of
104+fixation and reproducing fixations of the Work, including storage of a
105+protected performance or phonogram in digital form or other electronic
106+medium. 2. Fair Dealing Rights. Nothing in this License is intended to
107+reduce, limit, or restrict any uses free from copyright or rights
108+arising from limitations or exceptions that are provided for in
109+connection with the copyright protection under copyright law or other
110+applicable laws.
111+
112+3. License Grant. Subject to the terms and conditions of this License,
113+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
114+perpetual (for the duration of the applicable copyright) license to
115+exercise the rights in the Work as stated below:
116+
117+to Reproduce the Work, to incorporate the Work into one or more
118+Collections, and to Reproduce the Work as incorporated in the
119+Collections; to create and Reproduce Adaptations provided that any
120+such Adaptation, including any translation in any medium, takes
121+reasonable steps to clearly label, demarcate or otherwise identify
122+that changes were made to the original Work. For example, a
123+translation could be marked "The original work was translated from
124+English to Spanish," or a modification could indicate "The original
125+work has been modified."; to Distribute and Publicly Perform the Work
126+including as incorporated in Collections; and, to Distribute and
127+Publicly Perform Adaptations. For the avoidance of doubt:
128+
129+Non-waivable Compulsory License Schemes. In those jurisdictions in
130+which the right to collect royalties through any statutory or
131+compulsory licensing scheme cannot be waived, the Licensor reserves
132+the exclusive right to collect such royalties for any exercise by You
133+of the rights granted under this License; Waivable Compulsory License
134+Schemes. In those jurisdictions in which the right to collect
135+royalties through any statutory or compulsory licensing scheme can be
136+waived, the Licensor waives the exclusive right to collect such
137+royalties for any exercise by You of the rights granted under this
138+License; and, Voluntary License Schemes. The Licensor waives the right
139+to collect royalties, whether individually or, in the event that the
140+Licensor is a member of a collecting society that administers
141+voluntary licensing schemes, via that society, from any exercise by
142+You of the rights granted under this License. The above rights may be
143+exercised in all media and formats whether now known or hereafter
144+devised. The above rights include the right to make such modifications
145+as are technically necessary to exercise the rights in other media and
146+formats. Subject to Section 8(f), all rights not expressly granted by
147+Licensor are hereby reserved.
148+
149+4. Restrictions. The license granted in Section 3 above is expressly
150+made subject to and limited by the following restrictions:
151+
152+You may Distribute or Publicly Perform the Work only under the terms
153+of this License. You must include a copy of, or the Uniform Resource
154+Identifier (URI) for, this License with every copy of the Work You
155+Distribute or Publicly Perform. You may not offer or impose any terms
156+on the Work that restrict the terms of this License or the ability of
157+the recipient of the Work to exercise the rights granted to that
158+recipient under the terms of the License. You may not sublicense the
159+Work. You must keep intact all notices that refer to this License and
160+to the disclaimer of warranties with every copy of the Work You
161+Distribute or Publicly Perform. When You Distribute or Publicly
162+Perform the Work, You may not impose any effective technological
163+measures on the Work that restrict the ability of a recipient of the
164+Work from You to exercise the rights granted to that recipient under
165+the terms of the License. This Section 4(a) applies to the Work as
166+incorporated in a Collection, but this does not require the Collection
167+apart from the Work itself to be made subject to the terms of this
168+License. If You create a Collection, upon notice from any Licensor You
169+must, to the extent practicable, remove from the Collection any credit
170+as required by Section 4(b), as requested. If You create an
171+Adaptation, upon notice from any Licensor You must, to the extent
172+practicable, remove from the Adaptation any credit as required by
173+Section 4(b), as requested. If You Distribute, or Publicly Perform the
174+Work or any Adaptations or Collections, You must, unless a request has
175+been made pursuant to Section 4(a), keep intact all copyright notices
176+for the Work and provide, reasonable to the medium or means You are
177+utilizing: (i) the name of the Original Author (or pseudonym, if
178+applicable) if supplied, and/or if the Original Author and/or Licensor
179+designate another party or parties (e.g., a sponsor institute,
180+publishing entity, journal) for attribution ("Attribution Parties") in
181+Licensor's copyright notice, terms of service or by other reasonable
182+means, the name of such party or parties; (ii) the title of the Work
183+if supplied; (iii) to the extent reasonably practicable, the URI, if
184+any, that Licensor specifies to be associated with the Work, unless
185+such URI does not refer to the copyright notice or licensing
186+information for the Work; and (iv) , consistent with Section 3(b), in
187+the case of an Adaptation, a credit identifying the use of the Work in
188+the Adaptation (e.g., "French translation of the Work by Original
189+Author," or "Screenplay based on original Work by Original Author").
190+The credit required by this Section 4 (b) may be implemented in any
191+reasonable manner; provided, however, that in the case of a Adaptation
192+or Collection, at a minimum such credit will appear, if a credit for
193+all contributing authors of the Adaptation or Collection appears, then
194+as part of these credits and in a manner at least as prominent as the
195+credits for the other contributing authors. For the avoidance of
196+doubt, You may only use the credit required by this Section for the
197+purpose of attribution in the manner set out above and, by exercising
198+Your rights under this License, You may not implicitly or explicitly
199+assert or imply any connection with, sponsorship or endorsement by the
200+Original Author, Licensor and/or Attribution Parties, as appropriate,
201+of You or Your use of the Work, without the separate, express prior
202+written permission of the Original Author, Licensor and/or Attribution
203+Parties. Except as otherwise agreed in writing by the Licensor or as
204+may be otherwise permitted by applicable law, if You Reproduce,
205+Distribute or Publicly Perform the Work either by itself or as part of
206+any Adaptations or Collections, You must not distort, mutilate, modify
207+or take other derogatory action in relation to the Work which would be
208+prejudicial to the Original Author's honor or reputation. Licensor
209+agrees that in those jurisdictions (e.g. Japan), in which any exercise
210+of the right granted in Section 3(b) of this License (the right to
211+make Adaptations) would be deemed to be a distortion, mutilation,
212+modification or other derogatory action prejudicial to the Original
213+Author's honor and reputation, the Licensor will waive or not assert,
214+as appropriate, this Section, to the fullest extent permitted by the
215+applicable national law, to enable You to reasonably exercise Your
216+right under Section 3(b) of this License (right to make Adaptations)
217+but not otherwise. 5. Representations, Warranties and Disclaimer
218+
219+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
220+LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
221+WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
222+STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
223+TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
224+NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
225+OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE.
226+SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES,
227+SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
228+
229+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
230+APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
231+LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR
232+EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK,
233+EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
234+
235+7. Termination
236+
237+This License and the rights granted hereunder will terminate
238+automatically upon any breach by You of the terms of this License.
239+Individuals or entities who have received Adaptations or Collections
240+from You under this License, however, will not have their licenses
241+terminated provided such individuals or entities remain in full
242+compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
243+survive any termination of this License. Subject to the above terms
244+and conditions, the license granted here is perpetual (for the
245+duration of the applicable copyright in the Work). Notwithstanding the
246+above, Licensor reserves the right to release the Work under different
247+license terms or to stop distributing the Work at any time; provided,
248+however that any such election will not serve to withdraw this License
249+(or any other license that has been, or is required to be, granted
250+under the terms of this License), and this License will continue in
251+full force and effect unless terminated as stated above. 8.
252+Miscellaneous
253+
254+Each time You Distribute or Publicly Perform the Work or a Collection,
255+the Licensor offers to the recipient a license to the Work on the same
256+terms and conditions as the license granted to You under this License.
257+Each time You Distribute or Publicly Perform an Adaptation, Licensor
258+offers to the recipient a license to the original Work on the same
259+terms and conditions as the license granted to You under this License.
260+If any provision of this License is invalid or unenforceable under
261+applicable law, it shall not affect the validity or enforceability of
262+the remainder of the terms of this License, and without further action
263+by the parties to this agreement, such provision shall be reformed to
264+the minimum extent necessary to make such provision valid and
265+enforceable. No term or provision of this License shall be deemed
266+waived and no breach consented to unless such waiver or consent shall
267+be in writing and signed by the party to be charged with such waiver
268+or consent. This License constitutes the entire agreement between the
269+parties with respect to the Work licensed here. There are no
270+understandings, agreements or representations with respect to the Work
271+not specified here. Licensor shall not be bound by any additional
272+provisions that may appear in any communication from You. This License
273+may not be modified without the mutual written agreement of the
274+Licensor and You. The rights granted under, and the subject matter
275+referenced, in this License were drafted utilizing the terminology of
276+the Berne Convention for the Protection of Literary and Artistic Works
277+(as amended on September 28, 1979), the Rome Convention of 1961, the
278+WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms
279+Treaty of 1996 and the Universal Copyright Convention (as revised on
280+July 24, 1971). These rights and subject matter take effect in the
281+relevant jurisdiction in which the License terms are sought to be
282+enforced according to the corresponding provisions of the
283+implementation of those treaty provisions in the applicable national
284+law. If the standard suite of rights granted under applicable
285+copyright law includes additional rights not granted under this
286+License, such additional rights are deemed to be included in the
287+License; this License is not intended to restrict the license of any
288+rights under applicable law.
289+
290+=========================================================================
291\ No newline at end of file
292
293=== modified file 'lib/canonical/launchpad/icing/inline-sprites-1.css.in'
294--- lib/canonical/launchpad/icing/inline-sprites-1.css.in 2015-03-24 13:36:23 +0000
295+++ lib/canonical/launchpad/icing/inline-sprites-1.css.in 2015-06-25 03:34:41 +0000
296@@ -84,7 +84,11 @@
297 .branch {
298 background-image: url(/@@/branch.png); /* sprite-ref: icon-sprites */
299 background-repeat: no-repeat;
300- }
301+}
302+.gitbranch {
303+ background-image: url(/@@/gitbranch.png); /* sprite-ref: icon-sprites */
304+ background-repeat: no-repeat;
305+}
306 .distribution {
307 background-image: url(/@@/distribution.png); /* sprite-ref: icon-sprites */
308 background-repeat: no-repeat;
309
310=== modified file 'lib/canonical/launchpad/icing/style.css'
311--- lib/canonical/launchpad/icing/style.css 2015-04-07 23:43:43 +0000
312+++ lib/canonical/launchpad/icing/style.css 2015-06-25 03:34:41 +0000
313@@ -64,10 +64,24 @@
314 padding-left: 1em;
315 }
316
317+form label {
318+ font-weight: bold;
319+}
320+
321 form.primary.search {
322 margin-bottom: 2em;
323 }
324
325+.infobox {
326+ color: #666;
327+ display: inline-block;
328+ padding: 0.5em 0.5em 0.5em 2em;
329+ background: url(/@@/info) 0.5em 0.5em no-repeat;
330+ background-color: rgb(249, 249, 249);
331+ border: 1px solid #ddd;
332+ border-radius: 3px
333+}
334+
335 div#bugs-search-form.dynamic_bug_listing {
336 margin-bottom: 10px;
337 padding: 3px 0;
338@@ -538,17 +552,36 @@
339
340 /* --- Code --- */
341
342+code.command {
343+ background-color: #fff;
344+ border: 1px solid #ddd;
345+ border-radius: 3px;
346+ color: #626262;
347+ padding: 4px;
348+ font-family: "DejaVu Sans Mono", "Courier New", monospace;
349+ font-size: 1.05em;
350+}
351+code.command-block {
352+ display: block;
353+ margin-bottom: 1em;
354+ padding: 6px;
355+}
356+code.command-block:last-child {
357+ margin: 0;
358+}
359 table.code {
360 margin-bottom: 1em;
361- }
362+}
363 table.code th {
364 text-align: left;
365 font-weight: bold;
366- }
367+}
368 table.code th, table.code td {
369 padding-right: 3em;
370- }
371-
372+}
373+#git-expander-content {
374+ margin-top: 1em;
375+}
376 .branch-no-dev-focus {
377 background: #FFF59C;
378 vertical-align: middle;
379
380=== added file 'lib/canonical/launchpad/images/gitbranch-large.png'
381Binary files lib/canonical/launchpad/images/gitbranch-large.png 1970-01-01 00:00:00 +0000 and lib/canonical/launchpad/images/gitbranch-large.png 2015-06-25 03:34:41 +0000 differ
382=== added file 'lib/canonical/launchpad/images/gitbranch.png'
383Binary files lib/canonical/launchpad/images/gitbranch.png 1970-01-01 00:00:00 +0000 and lib/canonical/launchpad/images/gitbranch.png 2015-06-25 03:34:41 +0000 differ
384=== modified file 'lib/lp/code/browser/branchlisting.py'
385--- lib/lp/code/browser/branchlisting.py 2015-06-15 05:28:12 +0000
386+++ lib/lp/code/browser/branchlisting.py 2015-06-25 03:34:41 +0000
387@@ -1133,13 +1133,12 @@
388 @property
389 def configure_codehosting(self):
390 """Get the menu link for configuring code hosting."""
391- if not check_permission(
392- 'launchpad.Edit', self.context.development_focus):
393+ if not check_permission('launchpad.Edit', self.context):
394 return None
395- series_menu = MenuAPI(self.context.development_focus).overview
396- set_branch = series_menu['set_branch']
397- set_branch.text = 'Configure Code'
398- return set_branch
399+ menu = MenuAPI(self.context).overview
400+ configure_code = menu['configure_code']
401+ configure_code.text = 'Configure Code'
402+ return configure_code
403
404
405 class ProductBranchStatisticsView(BranchCountSummaryView,
406
407=== modified file 'lib/lp/code/browser/configure.zcml'
408--- lib/lp/code/browser/configure.zcml 2015-06-18 20:18:16 +0000
409+++ lib/lp/code/browser/configure.zcml 2015-06-25 03:34:41 +0000
410@@ -37,7 +37,34 @@
411 template="../templates/code-in-branches.pt"
412 permission="zope.Public"
413 />
414-
415+ <browser:page
416+ name="+configure-code"
417+ for="lp.registry.interfaces.product.IProduct"
418+ class="lp.registry.browser.product.ProductSetBranchView"
419+ permission="launchpad.Edit"
420+ template="../templates/configure-code.pt"
421+ />
422+ <browser:page
423+ name="+setbranch"
424+ for="lp.registry.interfaces.productseries.IProductSeries"
425+ class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
426+ permission="launchpad.Edit"
427+ template="../templates/configure-code.pt"
428+ />
429+ <browser:page
430+ name="+configure-code-macros"
431+ for="lp.registry.interfaces.productseries.IProductSeries"
432+ class="lp.app.browser.launchpad.Macro"
433+ permission="zope.Public"
434+ template="../templates/configure-code-macros.pt"
435+ />
436+ <browser:page
437+ name="+configure-code-macros"
438+ for="lp.registry.interfaces.product.IProduct"
439+ class="lp.app.browser.launchpad.Macro"
440+ permission="zope.Public"
441+ template="../templates/configure-code-macros.pt"
442+ />
443 <browser:page
444 for="lp.services.webapp.interfaces.ILaunchpadApplication"
445 name="+project-cloud"
446
447=== modified file 'lib/lp/code/javascript/productseries-setbranch.js'
448--- lib/lp/code/javascript/productseries-setbranch.js 2015-05-19 03:01:06 +0000
449+++ lib/lp/code/javascript/productseries-setbranch.js 2015-06-25 03:34:41 +0000
450@@ -24,6 +24,17 @@
451 return selected;
452 };
453
454+ module._get_selected_default_vcs = function () {
455+ var vcs = document.getElementsByName('field.default_vcs');
456+ var i;
457+ for (i = 0; i < vcs.length; i++) {
458+ if (vcs[i].checked) {
459+ return vcs[i].value;
460+ }
461+ }
462+ return null;
463+ };
464+
465
466 module.__rcs_types = null;
467
468@@ -39,6 +50,32 @@
469 field.disabled = !is_enabled;
470 };
471
472+ module.setup_expanders = function() {
473+ var git_expander = new Y.lp.app.widgets.expander.Expander(
474+ Y.one('#git-expander-icon'), Y.one('#git-expander-content')
475+ );
476+
477+ var bzr_expander = new Y.lp.app.widgets.expander.Expander(
478+ Y.one('#bzr-expander-icon'), Y.one('#bzr-expander-content')
479+ );
480+ module.git_expander = git_expander.setUp();
481+ module.bzr_expander = bzr_expander.setUp();
482+ };
483+
484+ module.onclick_default_vcs = function(e) {
485+ /* Which project vcs was selected? */
486+ var selectedDefaultVCS =
487+ module._get_selected_default_vcs();
488+
489+ if (selectedDefaultVCS === 'GIT') {
490+ module.git_expander.render(true);
491+ module.bzr_expander.render(false);
492+ } else {
493+ module.bzr_expander.render(true);
494+ module.git_expander.render(false);
495+ }
496+ };
497+
498 module.onclick_rcs_type = function(e) {
499 /* Which rcs type radio button has been selected? */
500 // CVS
501@@ -81,10 +118,14 @@
502 'click', module.onclick_rcs_type);
503 Y.all('input[name="field.branch_type"]').on(
504 'click', module.onclick_branch_type);
505+ Y.all('input[name="field.default_vcs"]').on(
506+ 'click', module.onclick_default_vcs);
507
508 // Set the initial state.
509+ module.setup_expanders();
510 module.onclick_rcs_type();
511 module.onclick_branch_type();
512+ module.onclick_default_vcs();
513 };
514
515 }, "0.1", {"requires": ["node", "DOM"]});
516
517=== modified file 'lib/lp/code/model/gitrepository.py'
518--- lib/lp/code/model/gitrepository.py 2015-06-15 09:55:47 +0000
519+++ lib/lp/code/model/gitrepository.py 2015-06-25 03:34:41 +0000
520@@ -296,7 +296,7 @@
521 repository_set = getUtility(IGitRepositorySet)
522 existing = repository_set.getDefaultRepositoryForOwner(
523 self.owner, self.target)
524- if existing is not None:
525+ if existing is not None and existing != self:
526 raise GitDefaultConflict(
527 existing, self.target, owner=self.owner)
528 self.owner_default = value
529@@ -307,7 +307,7 @@
530 # Check for an existing target default.
531 existing = getUtility(IGitRepositorySet).getDefaultRepository(
532 self.target)
533- if existing is not None:
534+ if existing is not None and existing != self:
535 raise GitDefaultConflict(existing, self.target)
536 self.target_default = value
537 if IProduct.providedBy(self.target):
538
539=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
540--- lib/lp/code/model/tests/test_gitrepository.py 2015-06-18 14:13:40 +0000
541+++ lib/lp/code/model/tests/test_gitrepository.py 2015-06-25 03:34:41 +0000
542@@ -1986,6 +1986,17 @@
543 self.repository_set.setDefaultRepositoryForOwner,
544 person, person, repository, user)
545
546+ def test_setDefaultRepository_for_same_targetdefault_noops(self):
547+ # If a repository is already the target default,
548+ # setting the defaultRepository again should no-op.
549+ project = self.factory.makeProduct()
550+ repository = self.factory.makeGitRepository(target=project)
551+ with person_logged_in(project.owner):
552+ self.repository_set.setDefaultRepository(project, repository)
553+ result = self.repository_set.setDefaultRepository(
554+ project, repository)
555+ self.assertEqual(None, result)
556+
557
558 class TestGitRepositorySetDefaultsMixin:
559
560
561=== modified file 'lib/lp/code/stories/codeimport/xx-create-codeimport.txt'
562--- lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-15 05:28:12 +0000
563+++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-25 03:34:41 +0000
564@@ -38,11 +38,12 @@
565 >>> owner_browser = setupBrowser(auth="Basic test@canonical.com:test")
566 >>> owner_browser.open('http://code.launchpad.dev/firefox')
567 >>> owner_browser.getLink('Configure Code').click()
568+ >>> owner_browser.getControl('Bazaar', index=0).click()
569 >>> owner_browser.getControl(
570 ... 'Import a branch').click()
571 >>> owner_browser.getControl('Branch URL').value = (
572 ... 'git://example.com/firefox')
573- >>> owner_browser.getControl('Git').click()
574+ >>> owner_browser.getControl('Git', index=1).click()
575 >>> owner_browser.getControl('Branch name').value = 'trunk'
576 >>> owner_browser.getControl('Update').click()
577
578
579=== added file 'lib/lp/code/templates/configure-code-macros.pt'
580--- lib/lp/code/templates/configure-code-macros.pt 1970-01-01 00:00:00 +0000
581+++ lib/lp/code/templates/configure-code-macros.pt 2015-06-25 03:34:41 +0000
582@@ -0,0 +1,39 @@
583+<tal:root
584+ xmlns:tal="http://xml.zope.org/namespaces/tal"
585+ xmlns:metal="http://xml.zope.org/namespaces/metal"
586+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
587+ omit-tag="">
588+
589+ <div metal:define-macro="push-instructions-bzr" id="push-instructions-bzr" class="scm-tip">
590+ <h3>Push a new branch</h3>
591+ <p>You can push a Bazaar branch directly to Launchpad with the command:</p>
592+ <p>
593+ <code class="command command-block">
594+ bzr push lp:<tal:project replace="context/name"/>
595+ </code>
596+ </p>
597+ </div>
598+
599+ <div metal:define-macro="push-instructions-git" id="push-instructions-git" class="scm-tip">
600+ <h3>Push a new repository</h3>
601+ <p>You can add a remote for your Git repository with the
602+ command:</p>
603+ <p>
604+ <code class="command command-block">
605+ git remote add origin <tal:project replace="modules/lp.services.config/config/codehosting/git_ssh_root"/><tal:project replace="context/name"/><br />
606+ </code></p>
607+ <p>&hellip; and push the Git branch to Launchpad with:</p>
608+ <p>
609+ <code class="command command-block">
610+ git push origin master
611+ </code>
612+ </p>
613+ </div>
614+
615+ <div metal:define-macro="no-keys" condition="not:view/user/sshkeys">
616+ <p class="infobox">To authenticate with the Launchpad branch upload service, you need to
617+ <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
618+ register an SSH key</a>.</p>
619+ </div>
620+
621+</tal:root>
622
623=== added file 'lib/lp/code/templates/configure-code.pt'
624--- lib/lp/code/templates/configure-code.pt 1970-01-01 00:00:00 +0000
625+++ lib/lp/code/templates/configure-code.pt 2015-06-25 03:34:41 +0000
626@@ -0,0 +1,165 @@
627+<html
628+ xmlns="http://www.w3.org/1999/xhtml"
629+ xmlns:tal="http://xml.zope.org/namespaces/tal"
630+ xmlns:metal="http://xml.zope.org/namespaces/metal"
631+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
632+ metal:use-macro="view/macro:page/main_only"
633+ i18n:domain="launchpad">
634+
635+ <body>
636+
637+ <metal:block fill-slot="head_epilogue">
638+ <style type="text/css">
639+ .subordinate {
640+ margin: 0.5em 0 0.5em 4em;
641+ }
642+ </style>
643+ </metal:block>
644+
645+ <div metal:fill-slot="main">
646+
647+ <div metal:use-macro="context/@@launchpad_form/form">
648+
649+ <metal:formbody fill-slot="widgets">
650+ <tal:block condition="not: view/is_series">
651+ <h3>Version control system</h3>
652+ <div id="default_vcs">
653+ <ul>
654+ <li>
655+ <label tal:replace="structure view/default_vcs_bzr">
656+ Bazaar
657+ </label>
658+ </li>
659+ <li>
660+ <label tal:replace="structure view/default_vcs_git">
661+ Git
662+ </label> <div style="display: inline"
663+ class="beta"><img alt="[BETA]" src="/@@/beta" /></div>
664+ </li>
665+ </ul>
666+ <p>Your project may have both Git repositories and Bazaar branches.
667+ </p>
668+ </div>
669+ </tal:block>
670+
671+ <div id="show-hide-bzr">
672+ <a href="#" id="bzr-expander-icon" class="expander-icon js-action">
673+ <span class="sprite branch">Bazaar settings</span>
674+ </a>
675+ <div id="bzr-expander-content">
676+ <div class="push-instructions">
677+ <div metal:use-macro="context/@@+configure-code-macros/push-instructions-bzr"></div>
678+ <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
679+ </div>
680+
681+ <table id="form_bzr" class="form">
682+ <tr>
683+ <td>
684+ <h3>Link or import an existing branch</h3>
685+ <label tal:replace="structure view/branch_type_link">
686+ Link to a Bazaar branch already on Launchpad
687+ </label>
688+ <table class="subordinate">
689+ <tal:widget define="widget nocall:view/widgets/branch_location">
690+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
691+ </tal:widget>
692+ </table>
693+ </td>
694+ </tr>
695+
696+ <tr id="branch_mirror">
697+ <td>
698+ <label tal:replace="structure view/branch_type_import">
699+ Import a branch hosted somewhere else
700+ </label>
701+ <table class="subordinate">
702+ <tal:widget define="widget nocall:view/widgets/branch_name">
703+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
704+ </tal:widget>
705+ <tal:widget define="widget nocall:view/widgets/branch_owner">
706+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
707+ </tal:widget>
708+
709+ <tal:widget define="widget nocall:view/widgets/repo_url">
710+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
711+ </tal:widget>
712+
713+ <tr>
714+ <td>
715+ <label tal:replace="structure view/rcs_type_bzr">
716+ Bazaar, hosted externally
717+ </label>
718+ </td>
719+ </tr>
720+
721+ <tr>
722+ <td>
723+ <label tal:replace="structure view/rcs_type_git">
724+ Git
725+ </label>
726+ </td>
727+ </tr>
728+
729+ <tr>
730+ <td>
731+ <label tal:replace="structure view/rcs_type_svn">
732+ SVN
733+ </label>
734+ </td>
735+ </tr>
736+
737+ <tr>
738+ <td>
739+ <label tal:replace="structure view/rcs_type_cvs">
740+ CVS
741+ </label>
742+ <table class="subordinate">
743+ <tal:widget define="widget nocall:view/widgets/cvs_module">
744+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
745+ </tal:widget>
746+ </table>
747+ </td>
748+ </tr>
749+ </table>
750+ </td>
751+ </tr>
752+ </table>
753+ </div>
754+ <input tal:replace="structure view/rcs_type_emptymarker" />
755+ </div>
756+
757+ <tal:block condition="not: view/is_series">
758+ <div id="show-hide-git">
759+ <a href="#" id="git-expander-icon" class="expander-icon js-action">
760+ <span class="sprite gitbranch">Git settings</span>
761+ </a>
762+ <div id="git-expander-content">
763+ <div id="form_git" class="form">
764+ <div class="push-instructions">
765+ <div metal:use-macro="context/@@+configure-code-macros/push-instructions-git"></div>
766+ <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
767+ </div>
768+
769+ <h3>Link an existing repository</h3>
770+ <p>Link to an existing Git repository already on Launchpad</p>
771+ <tal:widget define="widget nocall:view/widgets/git_repository_location">
772+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
773+ </tal:widget>
774+ </div>
775+ </div>
776+ </div>
777+ </tal:block>
778+
779+
780+ </metal:formbody>
781+ </div>
782+
783+ <script type="text/javascript">
784+ LPJS.use('lp.code.productseries_setbranch', function(Y) {
785+ Y.on('domready', Y.lp.code.productseries_setbranch.setup);
786+ });
787+ </script>
788+
789+ </div>
790+ </body>
791+</html>
792
793=== modified file 'lib/lp/code/templates/product-branch-summary.pt'
794--- lib/lp/code/templates/product-branch-summary.pt 2015-06-04 06:54:09 +0000
795+++ lib/lp/code/templates/product-branch-summary.pt 2015-06-25 03:34:41 +0000
796@@ -56,8 +56,7 @@
797 </p>
798 </div>
799
800- <tal:no-branches
801- condition="not: view/branch_count">
802+ <tal:no-branches condition="not: view/branch_count">
803 There are no branches for <tal:project-name replace="context/displayname"/>
804 in Launchpad.
805 <tal:can-configure condition="view/can_configure_branches">
806
807=== modified file 'lib/lp/code/vocabularies/configure.zcml'
808--- lib/lp/code/vocabularies/configure.zcml 2015-04-13 19:02:15 +0000
809+++ lib/lp/code/vocabularies/configure.zcml 2015-06-25 03:34:41 +0000
810@@ -66,4 +66,15 @@
811 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
812 </class>
813
814+ <securedutility
815+ name="GitRepositoryRestrictedOnProduct"
816+ component=".gitrepository.GitRepositoryRestrictedOnProductVocabulary"
817+ provides="zope.schema.interfaces.IVocabularyFactory">
818+ <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
819+ </securedutility>
820+
821+ <class class=".gitrepository.GitRepositoryRestrictedOnProductVocabulary">
822+ <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
823+ </class>
824+
825 </configure>
826
827=== modified file 'lib/lp/code/vocabularies/gitrepository.py'
828--- lib/lp/code/vocabularies/gitrepository.py 2015-04-28 15:22:46 +0000
829+++ lib/lp/code/vocabularies/gitrepository.py 2015-06-25 03:34:41 +0000
830@@ -6,6 +6,7 @@
831 __metaclass__ = type
832
833 __all__ = [
834+ 'GitRepositoryRestrictedOnProductVocabulary',
835 'GitRepositoryVocabulary',
836 ]
837
838@@ -15,6 +16,7 @@
839
840 from lp.code.interfaces.gitcollection import IAllGitRepositories
841 from lp.code.model.gitrepository import GitRepository
842+from lp.registry.interfaces.product import IProduct
843 from lp.services.webapp.interfaces import ILaunchBag
844 from lp.services.webapp.vocabulary import (
845 CountableIterator,
846@@ -62,3 +64,20 @@
847
848 def _getCollection(self):
849 return getUtility(IAllGitRepositories)
850+
851+class GitRepositoryRestrictedOnProductVocabulary(GitRepositoryVocabulary):
852+ """A vocabulary for searching git repositories restricted on product."""
853+
854+ def __init__(self, context):
855+ super(GitRepositoryRestrictedOnProductVocabulary, self).__init__(
856+ context)
857+ if IProduct.providedBy(self.context):
858+ self.product = self.context
859+ else:
860+ # An unexpected type.
861+ raise AssertionError('Unexpected context type')
862+
863+ def _getCollection(self):
864+ return getUtility(IAllGitRepositories).inProject(
865+ self.product).isExclusive()
866+
867
868=== modified file 'lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py'
869--- lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py 2015-05-14 13:57:51 +0000
870+++ lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py 2015-06-25 03:34:41 +0000
871@@ -5,9 +5,15 @@
872
873 __metaclass__ = type
874
875-from lp.code.vocabularies.gitrepository import GitRepositoryVocabulary
876+from zope.component import getUtility
877+
878+from lp.code.vocabularies.gitrepository import (
879+ GitRepositoryRestrictedOnProductVocabulary,
880+ GitRepositoryVocabulary,
881+ )
882 from lp.testing import TestCaseWithFactory
883 from lp.testing.layers import DatabaseFunctionalLayer
884+from lp.registry.interfaces.product import IProductSet
885
886
887 class TestGitRepositoryVocabulary(TestCaseWithFactory):
888@@ -50,3 +56,56 @@
889 # If there are more than one search result, a LookupError is still
890 # raised.
891 self.assertRaises(LookupError, self.vocab.getTermByToken, "fizzbuzz")
892+
893+
894+class TestRestrictedGitRepositoryVocabularyOnProduct(TestCaseWithFactory):
895+ """Test the GitRepositoryRestrictedOnProductVocabulary behaves as expected.
896+
897+ When a GitRepositoryRestrictedOnProductVocabulary is used with a project,
898+ the project of the git repository in the vocabulary match the product give
899+ as the context.
900+ """
901+
902+ layer = DatabaseFunctionalLayer
903+
904+ def setUp(self):
905+ super(TestRestrictedGitRepositoryVocabularyOnProduct, self).setUp()
906+ self._createRepositories()
907+ self.vocab = GitRepositoryRestrictedOnProductVocabulary(
908+ context=self._getVocabRestriction())
909+
910+ def _getVocabRestriction(self):
911+ """Restrict using the widget product."""
912+ return getUtility(IProductSet).getByName('widget')
913+
914+ def _createRepositories(self):
915+ test_product = self.factory.makeProduct(name='widget')
916+ other_product = self.factory.makeProduct(name='sprocket')
917+ person = self.factory.makePerson(name=u'scotty')
918+ self.factory.makeGitRepository(
919+ owner=person, target=test_product, name=u'mountain')
920+ self.factory.makeGitRepository(
921+ owner=person, target=other_product, name=u'mountain')
922+ self.product = test_product
923+ self.other_product = test_product
924+
925+ def test_product_restriction(self):
926+ """Look for widget's target default repository.
927+
928+ The result set should not show ~scotty/sprocket/mountain.
929+ """
930+ results = self.vocab.searchForTerms('mountain')
931+ expected = [u'~scotty/widget/+git/mountain']
932+ repo_names = sorted([repo.token for repo in results])
933+ self.assertEqual(expected, repo_names)
934+
935+ def test_singleQueryResult(self):
936+ # If there is a single search result that matches, use that
937+ # as the result.
938+ term = self.vocab.getTermByToken('mountain')
939+ self.assertEqual(u'~scotty/widget/+git/mountain', term.value.unique_name)
940+
941+ def test_multipleQueryResult(self):
942+ # If there are more than one search result, a LookupError is still
943+ # raised.
944+ self.assertRaises(LookupError, self.vocab.getTermByToken, 'scotty')
945
946=== modified file 'lib/lp/registry/browser/configure.zcml'
947--- lib/lp/registry/browser/configure.zcml 2015-06-10 10:19:25 +0000
948+++ lib/lp/registry/browser/configure.zcml 2015-06-25 03:34:41 +0000
949@@ -2002,13 +2002,6 @@
950 template="../templates/milestone-add.pt"
951 />
952 <browser:page
953- name="+setbranch"
954- for="lp.registry.interfaces.productseries.IProductSeries"
955- class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
956- permission="launchpad.Edit"
957- template="../templates/productseries-setbranch.pt"
958- />
959- <browser:page
960 name="+review"
961 for="lp.registry.interfaces.productseries.IProductSeries"
962 class="lp.registry.browser.productseries.ProductSeriesReviewView"
963
964=== modified file 'lib/lp/registry/browser/product.py'
965--- lib/lp/registry/browser/product.py 2015-06-15 05:28:12 +0000
966+++ lib/lp/registry/browser/product.py 2015-06-25 03:34:41 +0000
967@@ -1,4 +1,4 @@
968-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
969+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
970 # GNU Affero General Public License version 3 (see the file LICENSE).
971
972 """Browser views for products."""
973@@ -30,6 +30,7 @@
974 'ProductRdfView',
975 'ProductReviewLicenseView',
976 'ProductSeriesSetView',
977+ 'ProductSetBranchView',
978 'ProductSetBreadcrumb',
979 'ProductSetNavigation',
980 'ProductSetReviewLicensesView',
981@@ -44,8 +45,12 @@
982
983 from operator import attrgetter
984
985+from bzrlib.revision import NULL_REVISION
986 from lazr.delegates import delegates
987-from lazr.restful.interface import copy_field
988+from lazr.restful.interface import (
989+ copy_field,
990+ use_template,
991+ )
992 from lazr.restful.interfaces import IJSONRequestCache
993 from z3c.ptcompat import ViewPageTemplateFile
994 from zope.component import getUtility
995@@ -66,6 +71,7 @@
996 from zope.schema import (
997 Bool,
998 Choice,
999+ TextLine,
1000 )
1001 from zope.schema.vocabulary import (
1002 SimpleTerm,
1003@@ -80,6 +86,7 @@
1004 custom_widget,
1005 LaunchpadEditFormView,
1006 LaunchpadFormView,
1007+ render_radio_widget_part,
1008 ReturnToReferrerMixin,
1009 safe_action,
1010 )
1011@@ -103,7 +110,10 @@
1012 PUBLIC_PROPRIETARY_INFORMATION_TYPES,
1013 ServiceUsage,
1014 )
1015-from lp.app.errors import NotFoundError
1016+from lp.app.errors import (
1017+ NotFoundError,
1018+ UnexpectedFormData,
1019+ )
1020 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1021 from lp.app.utilities import json_dump_information_types
1022 from lp.app.vocabularies import InformationTypeVocabulary
1023@@ -131,9 +141,28 @@
1024 StructuralSubscriptionTargetTraversalMixin,
1025 )
1026 from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
1027+from lp.code.browser.branch import BranchNameValidationMixin
1028 from lp.code.browser.branchref import BranchRef
1029+from lp.code.browser.codeimport import validate_import_url
1030 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
1031+from lp.code.enums import (
1032+ BranchType,
1033+ RevisionControlSystems,
1034+ )
1035+from lp.code.errors import (
1036+ BranchCreationForbidden,
1037+ BranchExists,
1038+ )
1039+from lp.code.interfaces.branch import IBranch
1040+from lp.code.interfaces.branchjob import IRosettaUploadJobSource
1041+from lp.code.interfaces.branchtarget import IBranchTarget
1042+from lp.code.interfaces.codeimport import (
1043+ ICodeImport,
1044+ ICodeImportSet,
1045+ )
1046+from lp.code.interfaces.gitrepository import IGitRepositorySet
1047 from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
1048+
1049 from lp.registry.browser import (
1050 add_subscribe_link,
1051 BaseRdfView,
1052@@ -150,6 +179,7 @@
1053 PillarNavigationMixin,
1054 PillarViewMixin,
1055 )
1056+from lp.registry.enums import VCSType
1057 from lp.registry.interfaces.pillar import IPillarNameSet
1058 from lp.registry.interfaces.product import (
1059 IProduct,
1060@@ -171,6 +201,7 @@
1061 from lp.services.fields import (
1062 PillarAliases,
1063 PublicPersonChoice,
1064+ URIField,
1065 )
1066 from lp.services.librarian.interfaces import ILibraryFileAliasSet
1067 from lp.services.propertycache import cachedproperty
1068@@ -347,7 +378,6 @@
1069 'configured' -- a boolean representing the configuration status.
1070 """
1071 overview_menu = MenuAPI(self.context).overview
1072- series_menu = MenuAPI(self.context.development_focus).overview
1073 configuration_names = [
1074 'configure_bugtracker',
1075 'configure_translations',
1076@@ -363,11 +393,11 @@
1077 configured=config_statuses[key]))
1078
1079 # Add the branch configuration in separately.
1080- set_branch = series_menu['set_branch']
1081- set_branch.text = 'Code'
1082- set_branch.summary = "Specify the location of this project's code."
1083+ configure_code = overview_menu['configure_code']
1084+ configure_code.text = 'Code'
1085+ configure_code.summary = "Specify the location of this project's code."
1086 config_list.insert(0,
1087- dict(link=set_branch,
1088+ dict(link=configure_code,
1089 configured=config_statuses['configure_codehosting']))
1090 return config_list
1091
1092@@ -506,6 +536,7 @@
1093 'packages',
1094 'series',
1095 'series_add',
1096+ 'configure_code',
1097 'milestones',
1098 'downloads',
1099 'announce',
1100@@ -559,6 +590,14 @@
1101 'RDF</abbr> metadata')
1102 return Link('+rdf', text, icon='download')
1103
1104+ @enabled_with_permission('launchpad.Edit')
1105+ def configure_code(self):
1106+ """Return a link to configure code for this project."""
1107+ text = 'Configure code'
1108+ icon = 'edit'
1109+ summary = 'Configure code for this project'
1110+ return Link('+configure-code', text, summary, icon=icon)
1111+
1112 def downloads(self):
1113 text = 'Downloads'
1114 return Link('+download', text, icon='info')
1115@@ -1594,6 +1633,373 @@
1116 return BatchNavigator(decorated_result, self.request)
1117
1118
1119+LINK_LP_BZR = 'link-lp-bzr'
1120+IMPORT_EXTERNAL = 'import-external'
1121+
1122+
1123+BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
1124+ SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
1125+ _("Link to a Bazaar branch already on Launchpad")),
1126+ SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
1127+ _("Import a branch hosted somewhere else")),
1128+ ))
1129+
1130+
1131+class SetBranchForm(Interface):
1132+ """The fields presented on the form for setting a branch."""
1133+
1134+ use_template(ICodeImport, ['cvs_module'])
1135+
1136+ rcs_type = Choice(title=_("Type of RCS"),
1137+ required=False, vocabulary=RevisionControlSystems,
1138+ description=_(
1139+ "The version control system to import from. "))
1140+
1141+ repo_url = URIField(
1142+ title=_("Branch URL"), required=True,
1143+ description=_("The URL of the branch."),
1144+ allowed_schemes=["http", "https"],
1145+ allow_userinfo=False, allow_port=True, allow_query=False,
1146+ allow_fragment=False, trailing_slash=False)
1147+
1148+ branch_location = copy_field(
1149+ IProductSeries['branch'], __name__='branch_location',
1150+ title=_('Branch'),
1151+ description=_(
1152+ "The Bazaar branch for this series in Launchpad, "
1153+ "if one exists."))
1154+
1155+ branch_type = Choice(
1156+ title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
1157+ description=_("The type of import"), required=True)
1158+
1159+ branch_name = copy_field(
1160+ IBranch['name'], __name__='branch_name', title=_('Branch name'),
1161+ description=_(''), required=True)
1162+
1163+ branch_owner = copy_field(
1164+ IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
1165+ description=_(''), required=True)
1166+
1167+
1168+def create_git_fields():
1169+ return form.Fields(
1170+ Choice(__name__='default_vcs',
1171+ title=_("Project VCS"),
1172+ required=True, vocabulary=VCSType,
1173+ description=_("The version control system for "
1174+ "this project.")),
1175+ Choice(__name__='git_repository_location',
1176+ title=_('Git repository'),
1177+ required=False,
1178+ vocabulary='GitRepositoryRestrictedOnProduct',
1179+ description=_(
1180+ "The Git repository for this project in Launchpad, "
1181+ "if one exists, in the form: "
1182+ "~user/project-name/+git/repo-name"))
1183+ )
1184+
1185+
1186+class ProductSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
1187+ ProductView,
1188+ BranchNameValidationMixin):
1189+ """The view to set a branch default for the Product."""
1190+
1191+ label = 'Configure code'
1192+ page_title = label
1193+ schema = SetBranchForm
1194+ # Set for_input to True to ensure fields marked read-only will be editable
1195+ # upon creation.
1196+ for_input = True
1197+
1198+ custom_widget('rcs_type', LaunchpadRadioWidget)
1199+ custom_widget('branch_type', LaunchpadRadioWidget)
1200+ custom_widget('default_vcs', LaunchpadRadioWidget)
1201+
1202+ errors_in_action = False
1203+ is_series = False
1204+
1205+ @property
1206+ def series(self):
1207+ return self.context.development_focus
1208+
1209+ @property
1210+ def initial_values(self):
1211+ return dict(
1212+ rcs_type=RevisionControlSystems.BZR,
1213+ default_vcs=(self.context.pillar.inferred_vcs or VCSType.BZR),
1214+ branch_type=LINK_LP_BZR,
1215+ branch_location=self.series.branch)
1216+
1217+ @property
1218+ def next_url(self):
1219+ """Return the next_url.
1220+
1221+ Use the value from `ReturnToReferrerMixin` or None if there
1222+ are errors.
1223+ """
1224+ if self.errors_in_action:
1225+ return None
1226+ return super(ProductSetBranchView, self).next_url
1227+
1228+ def setUpFields(self):
1229+ """See `LaunchpadFormView`."""
1230+ super(ProductSetBranchView, self).setUpFields()
1231+ if not self.is_series:
1232+ self.form_fields = (self.form_fields + create_git_fields())
1233+
1234+ def setUpWidgets(self):
1235+ """See `LaunchpadFormView`."""
1236+ super(ProductSetBranchView, self).setUpWidgets()
1237+ widget = self.widgets['rcs_type']
1238+ vocab = widget.vocabulary
1239+ current_value = widget._getFormValue()
1240+ self.rcs_type_cvs = render_radio_widget_part(
1241+ widget, vocab.CVS, current_value, 'CVS')
1242+ self.rcs_type_svn = render_radio_widget_part(
1243+ widget, vocab.BZR_SVN, current_value, 'SVN')
1244+ self.rcs_type_git = render_radio_widget_part(
1245+ widget, vocab.GIT, current_value)
1246+ self.rcs_type_bzr = render_radio_widget_part(
1247+ widget, vocab.BZR, current_value)
1248+ self.rcs_type_emptymarker = widget._emptyMarker()
1249+
1250+ widget = self.widgets['branch_type']
1251+ current_value = widget._getFormValue()
1252+ vocab = widget.vocabulary
1253+
1254+ (self.branch_type_link,
1255+ self.branch_type_import) = [
1256+ render_radio_widget_part(widget, value, current_value)
1257+ for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
1258+
1259+ if not self.is_series:
1260+ widget = self.widgets['default_vcs']
1261+ vocab = widget.vocabulary
1262+ current_value = widget._getFormValue()
1263+ self.default_vcs_git = render_radio_widget_part(
1264+ widget, vocab.GIT, current_value, 'Git')
1265+ self.default_vcs_bzr = render_radio_widget_part(
1266+ widget, vocab.BZR, current_value, 'Bazaar')
1267+
1268+ def _validateLinkLpBzr(self, data):
1269+ """Validate data for link-lp-bzr case."""
1270+ if 'branch_location' not in data:
1271+ self.setFieldError(
1272+ 'branch_location', 'The branch location must be set.')
1273+
1274+ def _validateLinkLpGit(self, data):
1275+ """Validate data for link-lp-git case."""
1276+ if data.get('git_repository_location'):
1277+ repo = data.get('git_repository_location')
1278+ if not repo:
1279+ self.setFieldError(
1280+ 'git_repository_location',
1281+ 'The repository does not exist.')
1282+
1283+ def _validateImportExternal(self, data):
1284+ """Validate data for import external case."""
1285+ rcs_type = data.get('rcs_type')
1286+ repo_url = data.get('repo_url')
1287+
1288+ # Private teams are forbidden from owning code imports.
1289+ branch_owner = data.get('branch_owner')
1290+ if branch_owner is not None and branch_owner.private:
1291+ self.setFieldError(
1292+ 'branch_owner', 'Private teams are forbidden from owning '
1293+ 'external imports.')
1294+
1295+ if repo_url is None:
1296+ self.setFieldError(
1297+ 'repo_url', 'You must set the external repository URL.')
1298+ else:
1299+ reason = validate_import_url(repo_url, rcs_type)
1300+ if reason:
1301+ self.setFieldError('repo_url', reason)
1302+
1303+ # RCS type is mandatory.
1304+ # This condition should never happen since an initial value is set.
1305+ if rcs_type is None:
1306+ # The error shows but does not identify the widget.
1307+ self.setFieldError(
1308+ 'rcs_type',
1309+ 'You must specify the type of RCS for the remote host.')
1310+ elif rcs_type == RevisionControlSystems.CVS:
1311+ if 'cvs_module' not in data:
1312+ self.setFieldError('cvs_module', 'The CVS module must be set.')
1313+ self._validateBranch(data)
1314+
1315+ def _validateBranch(self, data):
1316+ """Validate that branch name and owner are set."""
1317+ if 'branch_name' not in data:
1318+ self.setFieldError('branch_name', 'The branch name must be set.')
1319+ if 'branch_owner' not in data:
1320+ self.setFieldError('branch_owner', 'The branch owner must be set.')
1321+
1322+ def _setRequired(self, names, value):
1323+ """Mark the widget field as optional."""
1324+ for name in names:
1325+ widget = self.widgets[name]
1326+ # The 'required' property on the widget context is set to False.
1327+ # The widget also has a 'required' property but it isn't used
1328+ # during validation.
1329+ widget.context.required = value
1330+
1331+ def _validSchemes(self, rcs_type):
1332+ """Return the valid schemes for the repository URL."""
1333+ schemes = set(['http', 'https'])
1334+ # Extend the allowed schemes for the repository URL based on
1335+ # rcs_type.
1336+ extra_schemes = {
1337+ RevisionControlSystems.BZR_SVN: ['svn'],
1338+ RevisionControlSystems.GIT: ['git'],
1339+ RevisionControlSystems.BZR: ['bzr'],
1340+ }
1341+ schemes.update(extra_schemes.get(rcs_type, []))
1342+ return schemes
1343+
1344+ def validate_widgets(self, data, names=None):
1345+ """See `LaunchpadFormView`."""
1346+ names = ['branch_type', 'rcs_type', 'default_vcs']
1347+ super(ProductSetBranchView, self).validate_widgets(data, names)
1348+ branch_type = data.get('branch_type')
1349+
1350+ if branch_type == LINK_LP_BZR:
1351+ # Mark other widgets as non-required.
1352+ self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
1353+ 'branch_name', 'branch_owner'], False)
1354+ elif branch_type == IMPORT_EXTERNAL:
1355+ rcs_type = data.get('rcs_type')
1356+
1357+ # Set the valid schemes based on rcs_type.
1358+ self.widgets['repo_url'].field.allowed_schemes = (
1359+ self._validSchemes(rcs_type))
1360+ # The branch location is not required for validation.
1361+ self._setRequired(['branch_location'], False)
1362+ # The cvs_module is required if it is a CVS import.
1363+ if rcs_type == RevisionControlSystems.CVS:
1364+ self._setRequired(['cvs_module'], True)
1365+ else:
1366+ raise AssertionError("Unknown branch type %s" % branch_type)
1367+ # Perform full validation now.
1368+ super(ProductSetBranchView, self).validate_widgets(data)
1369+
1370+ def validate(self, data):
1371+ """See `LaunchpadFormView`."""
1372+ # If widget validation returned errors then there is no need to
1373+ # continue as we'd likely just override the errors reported there.
1374+ if len(self.errors) > 0:
1375+ return
1376+ branch_type = data.get('branch_type')
1377+ if not self.is_series:
1378+ self._validateLinkLpGit(data)
1379+ if branch_type == IMPORT_EXTERNAL:
1380+ self._validateImportExternal(data)
1381+ elif branch_type == LINK_LP_BZR:
1382+ self._validateLinkLpBzr(data)
1383+ else:
1384+ raise AssertionError("Unknown branch type %s" % branch_type)
1385+
1386+ @property
1387+ def target(self):
1388+ """The branch target for the context."""
1389+ return IBranchTarget(self.context)
1390+
1391+ def abort_update(self):
1392+ """Abort transaction.
1393+
1394+ This is normally handled by LaunchpadFormView, but this can be called
1395+ from the success handler."""
1396+
1397+ self.errors_in_action = True
1398+ self._abort()
1399+
1400+ def add_update_notification(self):
1401+ self.request.response.addInfoNotification(
1402+ 'Project settings updated.')
1403+
1404+ @action(_('Update'), name='update')
1405+ def update_action(self, action, data):
1406+ branch_type = data.get('branch_type')
1407+
1408+ if not self.is_series:
1409+ default_vcs = data.get('default_vcs')
1410+ if default_vcs:
1411+ self.context.vcs = default_vcs
1412+
1413+ repo = data.get('git_repository_location')
1414+ getUtility(IGitRepositorySet).setDefaultRepository(
1415+ self.context, repo)
1416+ if branch_type == LINK_LP_BZR:
1417+ branch_location = data.get('branch_location')
1418+ if branch_location != self.series.branch:
1419+ self.series.branch = branch_location
1420+ # Request an initial upload of translation files.
1421+ getUtility(IRosettaUploadJobSource).create(
1422+ self.series.branch, NULL_REVISION)
1423+ else:
1424+ self.series.branch = branch_location
1425+ self.add_update_notification()
1426+ else:
1427+ branch_name = data.get('branch_name')
1428+ branch_owner = data.get('branch_owner')
1429+
1430+ if branch_type == IMPORT_EXTERNAL:
1431+ rcs_type = data.get('rcs_type')
1432+ if rcs_type == RevisionControlSystems.CVS:
1433+ cvs_root = data.get('repo_url')
1434+ cvs_module = data.get('cvs_module')
1435+ url = None
1436+ else:
1437+ cvs_root = None
1438+ cvs_module = None
1439+ url = data.get('repo_url')
1440+ rcs_item = RevisionControlSystems.items[rcs_type.name]
1441+ try:
1442+ code_import = getUtility(ICodeImportSet).new(
1443+ owner=branch_owner,
1444+ registrant=self.user,
1445+ target=IBranchTarget(self.target),
1446+ branch_name=branch_name,
1447+ rcs_type=rcs_item,
1448+ url=url,
1449+ cvs_root=cvs_root,
1450+ cvs_module=cvs_module)
1451+ except BranchExists as e:
1452+ self._setBranchExists(e.existing_branch, 'branch_name')
1453+ self.abort_update()
1454+ return
1455+ self.series.branch = code_import.branch
1456+ self.request.response.addInfoNotification(
1457+ 'Code import created and branch linked to the series.')
1458+ else:
1459+ raise UnexpectedFormData(branch_type)
1460+
1461+ def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
1462+ """Create a new hosted Bazaar branch.
1463+
1464+ Return the branch on success or None.
1465+ """
1466+ branch = None
1467+ try:
1468+ namespace = self.target.getNamespace(branch_owner)
1469+ branch = namespace.createBranch(
1470+ branch_type=BranchType.HOSTED, name=branch_name,
1471+ registrant=self.user, url=repo_url)
1472+ except BranchCreationForbidden:
1473+ self.addError(
1474+ "You are not allowed to create branches in %s." %
1475+ self.context.displayname)
1476+ except BranchExists as e:
1477+ self._setBranchExists(e.existing_branch, 'branch_name')
1478+ if branch is None:
1479+ self.errors_in_action = True
1480+ # Abort transaction. This is normally handled by
1481+ # LaunchpadFormView, but we are already in the success handler.
1482+ self._abort()
1483+ return branch
1484+
1485+
1486 class ProductRdfView(BaseRdfView):
1487 """A view that sets its mime-type to application/rdf+xml"""
1488
1489
1490=== modified file 'lib/lp/registry/browser/productseries.py'
1491--- lib/lp/registry/browser/productseries.py 2015-06-12 14:20:12 +0000
1492+++ lib/lp/registry/browser/productseries.py 2015-06-25 03:34:41 +0000
1493@@ -28,11 +28,6 @@
1494
1495 from operator import attrgetter
1496
1497-from bzrlib.revision import NULL_REVISION
1498-from lazr.restful.interface import (
1499- copy_field,
1500- use_template,
1501- )
1502 from z3c.ptcompat import ViewPageTemplateFile
1503 from zope.component import getUtility
1504 from zope.formlib import form
1505@@ -57,17 +52,11 @@
1506 custom_widget,
1507 LaunchpadEditFormView,
1508 LaunchpadFormView,
1509- render_radio_widget_part,
1510- ReturnToReferrerMixin,
1511 )
1512 from lp.app.browser.tales import MenuAPI
1513 from lp.app.enums import ServiceUsage
1514-from lp.app.errors import (
1515- NotFoundError,
1516- UnexpectedFormData,
1517- )
1518+from lp.app.errors import NotFoundError
1519 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1520-from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
1521 from lp.app.widgets.textwidgets import StrippedTextWidget
1522 from lp.blueprints.browser.specificationtarget import (
1523 HasSpecificationsMenuMixin,
1524@@ -81,24 +70,9 @@
1525 StructuralSubscriptionTargetTraversalMixin,
1526 )
1527 from lp.bugs.interfaces.bugtask import IBugTaskSet
1528-from lp.code.browser.branch import BranchNameValidationMixin
1529 from lp.code.browser.branchref import BranchRef
1530-from lp.code.browser.codeimport import validate_import_url
1531-from lp.code.enums import (
1532- BranchType,
1533- RevisionControlSystems,
1534- )
1535-from lp.code.errors import (
1536- BranchCreationForbidden,
1537- BranchExists,
1538- )
1539-from lp.code.interfaces.branch import IBranch
1540-from lp.code.interfaces.branchjob import IRosettaUploadJobSource
1541+from lp.code.enums import RevisionControlSystems
1542 from lp.code.interfaces.branchtarget import IBranchTarget
1543-from lp.code.interfaces.codeimport import (
1544- ICodeImport,
1545- ICodeImportSet,
1546- )
1547 from lp.registry.browser import (
1548 add_subscribe_link,
1549 BaseRdfView,
1550@@ -110,6 +84,7 @@
1551 InvolvedMenu,
1552 PillarInvolvementView,
1553 )
1554+from lp.registry.browser.product import ProductSetBranchView
1555 from lp.registry.errors import CannotPackageProprietaryProduct
1556 from lp.registry.interfaces.packaging import (
1557 IPackaging,
1558@@ -117,7 +92,6 @@
1559 )
1560 from lp.registry.interfaces.productseries import IProductSeries
1561 from lp.registry.interfaces.series import SeriesStatus
1562-from lp.services.fields import URIField
1563 from lp.services.propertycache import cachedproperty
1564 from lp.services.webapp import (
1565 ApplicationMenu,
1566@@ -756,300 +730,23 @@
1567 self.next_url = canonical_url(product)
1568
1569
1570-LINK_LP_BZR = 'link-lp-bzr'
1571-IMPORT_EXTERNAL = 'import-external'
1572-
1573-
1574-BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
1575- SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
1576- _("Link to a Bazaar branch already on Launchpad")),
1577- SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
1578- _("Import a branch hosted somewhere else")),
1579- ))
1580-
1581-
1582-class SetBranchForm(Interface):
1583- """The fields presented on the form for setting a branch."""
1584-
1585- use_template(ICodeImport, ['cvs_module'])
1586-
1587- rcs_type = Choice(title=_("Type of RCS"),
1588- required=False, vocabulary=RevisionControlSystems,
1589- description=_(
1590- "The version control system to import from. "))
1591-
1592- repo_url = URIField(
1593- title=_("Branch URL"), required=True,
1594- description=_("The URL of the branch."),
1595- allowed_schemes=["http", "https"],
1596- allow_userinfo=False, allow_port=True, allow_query=False,
1597- allow_fragment=False, trailing_slash=False)
1598-
1599- branch_location = copy_field(
1600- IProductSeries['branch'], __name__='branch_location',
1601- title=_('Branch'),
1602- description=_(
1603- "The Bazaar branch for this series in Launchpad, "
1604- "if one exists."))
1605-
1606- branch_type = Choice(
1607- title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
1608- description=_("The type of import"), required=True)
1609-
1610- branch_name = copy_field(
1611- IBranch['name'], __name__='branch_name', title=_('Branch name'),
1612- description=_(''), required=True)
1613-
1614- branch_owner = copy_field(
1615- IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
1616- description=_(''), required=True)
1617-
1618-
1619-class ProductSeriesSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
1620- ProductSeriesView,
1621- BranchNameValidationMixin):
1622+class ProductSeriesSetBranchView(ProductSetBranchView, ProductSeriesView):
1623 """The view to set a branch for the ProductSeries."""
1624
1625- schema = SetBranchForm
1626- # Set for_input to True to ensure fields marked read-only will be editable
1627- # upon creation.
1628- for_input = True
1629-
1630- custom_widget('rcs_type', LaunchpadRadioWidget)
1631- custom_widget('branch_type', LaunchpadRadioWidget)
1632-
1633- errors_in_action = False
1634-
1635- @property
1636- def initial_values(self):
1637- return dict(
1638- rcs_type=RevisionControlSystems.BZR,
1639- branch_type=LINK_LP_BZR,
1640- branch_location=self.context.branch)
1641-
1642- @property
1643- def next_url(self):
1644- """Return the next_url.
1645-
1646- Use the value from `ReturnToReferrerMixin` or None if there
1647- are errors.
1648- """
1649- if self.errors_in_action:
1650- return None
1651- return super(ProductSeriesSetBranchView, self).next_url
1652-
1653- def setUpWidgets(self):
1654- """See `LaunchpadFormView`."""
1655- super(ProductSeriesSetBranchView, self).setUpWidgets()
1656- widget = self.widgets['rcs_type']
1657- vocab = widget.vocabulary
1658- current_value = widget._getFormValue()
1659- self.rcs_type_cvs = render_radio_widget_part(
1660- widget, vocab.CVS, current_value, 'CVS')
1661- self.rcs_type_svn = render_radio_widget_part(
1662- widget, vocab.BZR_SVN, current_value, 'SVN')
1663- self.rcs_type_git = render_radio_widget_part(
1664- widget, vocab.GIT, current_value)
1665- self.rcs_type_bzr = render_radio_widget_part(
1666- widget, vocab.BZR, current_value)
1667- self.rcs_type_emptymarker = widget._emptyMarker()
1668-
1669- widget = self.widgets['branch_type']
1670- current_value = widget._getFormValue()
1671- vocab = widget.vocabulary
1672-
1673- (self.branch_type_link,
1674- self.branch_type_import) = [
1675- render_radio_widget_part(widget, value, current_value)
1676- for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
1677-
1678- def _validateLinkLpBzr(self, data):
1679- """Validate data for link-lp-bzr case."""
1680- if 'branch_location' not in data:
1681- self.setFieldError(
1682- 'branch_location', 'The branch location must be set.')
1683-
1684- def _validateImportExternal(self, data):
1685- """Validate data for import external case."""
1686- rcs_type = data.get('rcs_type')
1687- repo_url = data.get('repo_url')
1688-
1689- # Private teams are forbidden from owning code imports.
1690- branch_owner = data.get('branch_owner')
1691- if branch_owner is not None and branch_owner.private:
1692- self.setFieldError(
1693- 'branch_owner', 'Private teams are forbidden from owning '
1694- 'external imports.')
1695-
1696- if repo_url is None:
1697- self.setFieldError(
1698- 'repo_url', 'You must set the external repository URL.')
1699- else:
1700- reason = validate_import_url(repo_url, rcs_type)
1701- if reason:
1702- self.setFieldError('repo_url', reason)
1703-
1704- # RCS type is mandatory.
1705- # This condition should never happen since an initial value is set.
1706- if rcs_type is None:
1707- # The error shows but does not identify the widget.
1708- self.setFieldError(
1709- 'rcs_type',
1710- 'You must specify the type of RCS for the remote host.')
1711- elif rcs_type == RevisionControlSystems.CVS:
1712- if 'cvs_module' not in data:
1713- self.setFieldError('cvs_module', 'The CVS module must be set.')
1714- self._validateBranch(data)
1715-
1716- def _validateBranch(self, data):
1717- """Validate that branch name and owner are set."""
1718- if 'branch_name' not in data:
1719- self.setFieldError('branch_name', 'The branch name must be set.')
1720- if 'branch_owner' not in data:
1721- self.setFieldError('branch_owner', 'The branch owner must be set.')
1722-
1723- def _setRequired(self, names, value):
1724- """Mark the widget field as optional."""
1725- for name in names:
1726- widget = self.widgets[name]
1727- # The 'required' property on the widget context is set to False.
1728- # The widget also has a 'required' property but it isn't used
1729- # during validation.
1730- widget.context.required = value
1731-
1732- def _validSchemes(self, rcs_type):
1733- """Return the valid schemes for the repository URL."""
1734- schemes = set(['http', 'https'])
1735- # Extend the allowed schemes for the repository URL based on
1736- # rcs_type.
1737- extra_schemes = {
1738- RevisionControlSystems.BZR_SVN: ['svn'],
1739- RevisionControlSystems.GIT: ['git'],
1740- RevisionControlSystems.BZR: ['bzr'],
1741- }
1742- schemes.update(extra_schemes.get(rcs_type, []))
1743- return schemes
1744-
1745- def validate_widgets(self, data, names=None):
1746- """See `LaunchpadFormView`."""
1747- names = ['branch_type', 'rcs_type']
1748- super(ProductSeriesSetBranchView, self).validate_widgets(data, names)
1749- branch_type = data.get('branch_type')
1750- if branch_type == LINK_LP_BZR:
1751- # Mark other widgets as non-required.
1752- self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
1753- 'branch_name', 'branch_owner'], False)
1754- elif branch_type == IMPORT_EXTERNAL:
1755- rcs_type = data.get('rcs_type')
1756-
1757- # Set the valid schemes based on rcs_type.
1758- self.widgets['repo_url'].field.allowed_schemes = (
1759- self._validSchemes(rcs_type))
1760- # The branch location is not required for validation.
1761- self._setRequired(['branch_location'], False)
1762- # The cvs_module is required if it is a CVS import.
1763- if rcs_type == RevisionControlSystems.CVS:
1764- self._setRequired(['cvs_module'], True)
1765- else:
1766- raise AssertionError("Unknown branch type %s" % branch_type)
1767- # Perform full validation now.
1768- super(ProductSeriesSetBranchView, self).validate_widgets(data)
1769-
1770- def validate(self, data):
1771- """See `LaunchpadFormView`."""
1772- # If widget validation returned errors then there is no need to
1773- # continue as we'd likely just override the errors reported there.
1774- if len(self.errors) > 0:
1775- return
1776- branch_type = data['branch_type']
1777- if branch_type == IMPORT_EXTERNAL:
1778- self._validateImportExternal(data)
1779- elif branch_type == LINK_LP_BZR:
1780- self._validateLinkLpBzr(data)
1781- else:
1782- raise AssertionError("Unknown branch type %s" % branch_type)
1783+ is_series = True
1784+
1785+ @property
1786+ def series(self):
1787+ return self.context
1788
1789 @property
1790 def target(self):
1791 """The branch target for the context."""
1792 return IBranchTarget(self.context.product)
1793
1794- @action(_('Update'), name='update')
1795- def update_action(self, action, data):
1796- branch_type = data.get('branch_type')
1797- if branch_type == LINK_LP_BZR:
1798- branch_location = data.get('branch_location')
1799- if branch_location != self.context.branch:
1800- self.context.branch = branch_location
1801- # Request an initial upload of translation files.
1802- getUtility(IRosettaUploadJobSource).create(
1803- self.context.branch, NULL_REVISION)
1804- else:
1805- self.context.branch = branch_location
1806- self.request.response.addInfoNotification(
1807- 'Series code location updated.')
1808- else:
1809- branch_name = data.get('branch_name')
1810- branch_owner = data.get('branch_owner')
1811-
1812- if branch_type == IMPORT_EXTERNAL:
1813- rcs_type = data.get('rcs_type')
1814- if rcs_type == RevisionControlSystems.CVS:
1815- cvs_root = data.get('repo_url')
1816- cvs_module = data.get('cvs_module')
1817- url = None
1818- else:
1819- cvs_root = None
1820- cvs_module = None
1821- url = data.get('repo_url')
1822- rcs_item = RevisionControlSystems.items[rcs_type.name]
1823- try:
1824- code_import = getUtility(ICodeImportSet).new(
1825- owner=branch_owner,
1826- registrant=self.user,
1827- target=IBranchTarget(self.context.product),
1828- branch_name=branch_name,
1829- rcs_type=rcs_item,
1830- url=url,
1831- cvs_root=cvs_root,
1832- cvs_module=cvs_module)
1833- except BranchExists as e:
1834- self._setBranchExists(e.existing_branch, 'branch_name')
1835- self.errors_in_action = True
1836- # Abort transaction. This is normally handled
1837- # by LaunchpadFormView, but we are already in
1838- # the success handler.
1839- self._abort()
1840- return
1841- self.context.branch = code_import.branch
1842- self.request.response.addInfoNotification(
1843- 'Code import created and branch linked to the series.')
1844- else:
1845- raise UnexpectedFormData(branch_type)
1846-
1847- def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
1848- """Create a new hosted Bazaar branch.
1849-
1850- Return the branch on success or None.
1851- """
1852- branch = None
1853- try:
1854- namespace = self.target.getNamespace(branch_owner)
1855- branch = namespace.createBranch(
1856- branch_type=BranchType.HOSTED, name=branch_name,
1857- registrant=self.user, url=repo_url)
1858- except BranchCreationForbidden:
1859- self.addError(
1860- "You are not allowed to create branches in %s." %
1861- self.context.displayname)
1862- except BranchExists as e:
1863- self._setBranchExists(e.existing_branch, 'branch_name')
1864- if branch is None:
1865- self.errors_in_action = True
1866- # Abort transaction. This is normally handled by
1867- # LaunchpadFormView, but we are already in the success handler.
1868- self._abort()
1869- return branch
1870+ def add_update_notification(self):
1871+ self.request.response.addInfoNotification(
1872+ 'Series code location updated.')
1873
1874
1875 class ProductSeriesReviewView(LaunchpadEditFormView):
1876
1877=== modified file 'lib/lp/registry/browser/tests/productseries-setbranch-view.txt'
1878--- lib/lp/registry/browser/tests/productseries-setbranch-view.txt 2015-06-12 14:20:12 +0000
1879+++ lib/lp/registry/browser/tests/productseries-setbranch-view.txt 2015-06-25 03:34:41 +0000
1880@@ -27,8 +27,8 @@
1881
1882 The user can see instructions to push a branch.
1883
1884- >>> instructions = find_tag_by_id(content, 'push-instructions')
1885- >>> 'bzr push lp:~' in str(instructions)
1886+ >>> instructions = find_tag_by_id(content, 'push-instructions-bzr')
1887+ >>> 'bzr push lp:' in str(instructions)
1888 True
1889
1890
1891
1892=== modified file 'lib/lp/registry/browser/tests/test_product.py'
1893--- lib/lp/registry/browser/tests/test_product.py 2014-06-19 06:38:53 +0000
1894+++ lib/lp/registry/browser/tests/test_product.py 2015-06-25 03:34:41 +0000
1895@@ -34,6 +34,7 @@
1896 from lp.registry.enums import (
1897 EXCLUSIVE_TEAM_POLICY,
1898 TeamMembershipPolicy,
1899+ VCSType,
1900 )
1901 from lp.registry.interfaces.product import (
1902 IProductSet,
1903@@ -309,6 +310,12 @@
1904 view = create_initialized_view(self.product, '+index')
1905 self.assertTrue(view.show_programming_languages)
1906
1907+ def test_show_default_vcs(self):
1908+ with person_logged_in(self.product.owner):
1909+ self.product.vcs = VCSType.GIT
1910+ view = create_initialized_view(self.product, '+index')
1911+ self.assertTrue(view.show_vcs)
1912+
1913 def test_show_license_info_without_other_license(self):
1914 # show_license_info is false when one of the "other" licences is
1915 # not selected.
1916
1917=== added file 'lib/lp/registry/browser/tests/test_product_views.py'
1918--- lib/lp/registry/browser/tests/test_product_views.py 1970-01-01 00:00:00 +0000
1919+++ lib/lp/registry/browser/tests/test_product_views.py 2015-06-25 03:34:41 +0000
1920@@ -0,0 +1,36 @@
1921+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
1922+# GNU Affero General Public License version 3 (see the file LICENSE).
1923+
1924+"""View tests for Product pages."""
1925+
1926+__metaclass__ = type
1927+
1928+import soupmatchers
1929+from zope.security.proxy import removeSecurityProxy
1930+
1931+from lp.services.webapp import canonical_url
1932+from lp.testing import BrowserTestCase
1933+from lp.testing.layers import DatabaseFunctionalLayer
1934+
1935+
1936+class TestProductSetBranchView(BrowserTestCase):
1937+
1938+ layer = DatabaseFunctionalLayer
1939+
1940+ def getBrowser(self, project, view_name=None):
1941+ project = removeSecurityProxy(project)
1942+ url = canonical_url(project, view_name=view_name)
1943+ return self.getUserBrowser(url, project.owner)
1944+
1945+ def test_link_existing_git_repository(self):
1946+ repo = removeSecurityProxy(self.factory.makeGitRepository(
1947+ target=self.factory.makeProduct()))
1948+ browser = self.getBrowser(repo.project, '+configure-code')
1949+ browser.getControl('Git', index=0).click()
1950+ browser.getControl('Git repository').value = repo.shortened_path
1951+ browser.getControl('Update').click()
1952+
1953+ tag = soupmatchers.Tag(
1954+ 'success-div', 'div', attrs={'class': 'informational message'},
1955+ text='Project settings updated.')
1956+ self.assertThat(browser.contents, soupmatchers.HTMLContains(tag))
1957
1958=== modified file 'lib/lp/registry/interfaces/product.py'
1959--- lib/lp/registry/interfaces/product.py 2015-05-13 05:03:33 +0000
1960+++ lib/lp/registry/interfaces/product.py 2015-06-25 03:34:41 +0000
1961@@ -761,6 +761,14 @@
1962 description=_(
1963 "Version control system for this project's code.")))
1964
1965+ inferred_vcs = exported(
1966+ Choice(
1967+ title=_("Inferred VCS"),
1968+ readonly=True,
1969+ vocabulary=VCSType,
1970+ description=_(
1971+ "Inferred version control system for this project's code.")))
1972+
1973 def getAllowedBugInformationTypes():
1974 """Get the information types that a bug in this project can have.
1975
1976
1977=== modified file 'lib/lp/registry/model/product.py'
1978--- lib/lp/registry/model/product.py 2015-05-13 05:25:30 +0000
1979+++ lib/lp/registry/model/product.py 2015-06-25 03:34:41 +0000
1980@@ -116,6 +116,8 @@
1981 )
1982 from lp.code.enums import BranchType
1983 from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
1984+from lp.code.interfaces.branchcollection import IBranchCollection
1985+from lp.code.interfaces.gitcollection import IGitCollection
1986 from lp.code.interfaces.gitrepository import IGitRepositorySet
1987 from lp.code.model.branch import Branch
1988 from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
1989@@ -445,6 +447,19 @@
1990 """
1991 return None
1992
1993+ @property
1994+ def inferred_vcs(self):
1995+ """Use vcs, otherwise infer from existence of git or bzr branches.
1996+
1997+ Bzr take precedence over git, if no project vcs set.
1998+ """
1999+ if self.vcs:
2000+ return self.vcs
2001+ if not IBranchCollection(self).is_empty():
2002+ return VCSType.BZR
2003+ elif not IGitCollection(self).is_empty():
2004+ return VCSType.GIT
2005+
2006 @date_next_suggest_packaging.setter # pyflakes:ignore
2007 def date_next_suggest_packaging(self, value):
2008 """See `IProduct`
2009
2010=== modified file 'lib/lp/registry/stories/product/xx-product-development-focus.txt'
2011--- lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-14 23:48:24 +0000
2012+++ lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-25 03:34:41 +0000
2013@@ -74,7 +74,7 @@
2014 (http://launchpad.dev/fooix/+edit)
2015 >>> print_involvement_portlet(owner_browser)
2016 Code
2017- http://launchpad.dev/fooix/trunk/+setbranch
2018+ http://launchpad.dev/fooix/+configure-code
2019 Bugs
2020 http://launchpad.dev/fooix/+configure-bugtracker
2021 Translations
2022@@ -84,12 +84,13 @@
2023
2024 The owner can specify the development focus branch from the overview page.
2025
2026- >>> owner_browser.getLink(url='+setbranch').click()
2027+ >>> owner_browser.getLink(url='+configure-code').click()
2028+ >>> owner_browser.getControl('Bazaar', index=0).click()
2029 >>> owner_browser.getControl(name='field.branch_location').value = (
2030 ... '~eric/fooix/trunk')
2031 >>> owner_browser.getControl('Update').click()
2032 >>> print_feedback_messages(owner_browser.contents)
2033- Series code location updated.
2034+ Project settings updated.
2035
2036 The owner is taken back to the project page.
2037
2038
2039=== modified file 'lib/lp/registry/templates/product-index.pt'
2040--- lib/lp/registry/templates/product-index.pt 2015-01-29 16:28:30 +0000
2041+++ lib/lp/registry/templates/product-index.pt 2015-06-25 03:34:41 +0000
2042@@ -129,6 +129,11 @@
2043 <dd tal:content="structure view/languages_edit_widget" />
2044 </dl>
2045
2046+ <dl id="product-vcs" tal:condition="context/inferred_vcs">
2047+ <dt>Version control system:</dt>
2048+ <dd tal:content="context/inferred_vcs/title">Git</dd>
2049+ </dl>
2050+
2051 <dl id="licences">
2052 <dt>Licences:</dt>
2053 <dd>
2054
2055=== removed file 'lib/lp/registry/templates/productseries-setbranch.pt'
2056--- lib/lp/registry/templates/productseries-setbranch.pt 2015-05-01 13:18:54 +0000
2057+++ lib/lp/registry/templates/productseries-setbranch.pt 1970-01-01 00:00:00 +0000
2058@@ -1,125 +0,0 @@
2059-<html
2060- xmlns="http://www.w3.org/1999/xhtml"
2061- xmlns:tal="http://xml.zope.org/namespaces/tal"
2062- xmlns:metal="http://xml.zope.org/namespaces/metal"
2063- xmlns:i18n="http://xml.zope.org/namespaces/i18n"
2064- metal:use-macro="view/macro:page/main_only"
2065- i18n:domain="launchpad">
2066-
2067-<body>
2068-
2069-<metal:block fill-slot="head_epilogue">
2070- <style type="text/css">
2071- .subordinate {
2072- margin: 0.5em 0 0.5em 4em;
2073- }
2074- </style>
2075-</metal:block>
2076-
2077-<div metal:fill-slot="main">
2078-
2079- <p id="push-instructions">
2080- You can push a Bazaar branch directly to Launchpad with the command:<br />
2081- <tt class="command">
2082- bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/<tal:series replace="context/name"/>
2083- </tt>
2084- <tal:no-keys condition="not:view/user/sshkeys">
2085- <br/>To authenticate with the Launchpad branch upload service,
2086- you need to
2087- <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
2088- register a SSH key</a>.
2089- </tal:no-keys>
2090- </p>
2091-
2092- <div metal:use-macro="context/@@launchpad_form/form">
2093-
2094- <metal:formbody fill-slot="widgets">
2095-
2096- <table class="form">
2097-
2098- <tr>
2099- <td>
2100- <label tal:replace="structure view/branch_type_link">
2101- Link to a Bazaar branch already in Launchpad
2102- </label>
2103- <table class="subordinate">
2104- <tal:widget define="widget nocall:view/widgets/branch_location">
2105- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2106- </tal:widget>
2107- </table>
2108- </td>
2109- </tr>
2110-
2111- <tr>
2112- <td>
2113- <label tal:replace="structure view/branch_type_import">
2114- Import a branch hosted somewhere else
2115- </label>
2116- <table class="subordinate">
2117- <tal:widget define="widget nocall:view/widgets/repo_url">
2118- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2119- </tal:widget>
2120-
2121- <tr>
2122- <td>
2123- <label tal:replace="structure view/rcs_type_bzr">
2124- Bazaar, hosted externally
2125- </label>
2126- </td>
2127- </tr>
2128-
2129- <tr>
2130- <td>
2131- <label tal:replace="structure view/rcs_type_git">
2132- Git
2133- </label>
2134- </td>
2135- </tr>
2136-
2137- <tr>
2138- <td>
2139- <label tal:replace="structure view/rcs_type_svn">
2140- SVN
2141- </label>
2142- </td>
2143- </tr>
2144-
2145- <tr>
2146- <td>
2147- <label tal:replace="structure view/rcs_type_cvs">
2148- CVS
2149- </label>
2150- <table class="subordinate">
2151- <tal:widget define="widget nocall:view/widgets/cvs_module">
2152- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2153- </tal:widget>
2154- </table>
2155- </td>
2156- </tr>
2157-
2158- </table>
2159- </td>
2160- </tr>
2161-
2162- <tal:widget define="widget nocall:view/widgets/branch_name">
2163- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2164- </tal:widget>
2165- <tal:widget define="widget nocall:view/widgets/branch_owner">
2166- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2167- </tal:widget>
2168-
2169- </table>
2170- <input tal:replace="structure view/rcs_type_emptymarker" />
2171-
2172- </metal:formbody>
2173- </div>
2174-
2175- <script type="text/javascript">
2176- LPJS.use('lp.code.productseries_setbranch', function(Y) {
2177- Y.on('domready', Y.lp.code.productseries_setbranch.setup);
2178- });
2179- </script>
2180-
2181-</div>
2182-</body>
2183-</html>
2184
2185=== modified file 'lib/lp/registry/tests/test_product.py'
2186--- lib/lp/registry/tests/test_product.py 2015-05-13 05:03:33 +0000
2187+++ lib/lp/registry/tests/test_product.py 2015-06-25 03:34:41 +0000
2188@@ -65,6 +65,7 @@
2189 SharingPermission,
2190 SpecificationSharingPolicy,
2191 TeamMembershipPolicy,
2192+ VCSType,
2193 )
2194 from lp.registry.errors import (
2195 CannotChangeInformationType,
2196@@ -309,6 +310,13 @@
2197 [u'trunk', u'active-series'],
2198 [series.name for series in active_series])
2199
2200+ def test_inferred_vcs(self):
2201+ """VCS is inferred correctly from existing branch or repo."""
2202+ git_repo = self.factory.makeGitRepository()
2203+ bzr_branch = self.factory.makeBranch()
2204+ self.assertEqual(VCSType.GIT, git_repo.target.inferred_vcs)
2205+ self.assertEqual(VCSType.BZR, bzr_branch.product.inferred_vcs)
2206+
2207 def test_owner_cannot_be_open_team(self):
2208 """Product owners cannot be open teams."""
2209 for policy in INCLUSIVE_TEAM_POLICY: