Merge lp:~blr/launchpad/project-meta-go-import into lp:launchpad

Proposed by Kit Randel on 2015-06-16
Status: Superseded
Proposed branch: lp:~blr/launchpad/project-meta-go-import
Merge into: lp:launchpad
Diff against target: 2088 lines (+1150/-471)
21 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/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 (+167/-0)
lib/lp/code/templates/product-branch-summary.pt (+1/-2)
lib/lp/registry/browser/configure.zcml (+0/-7)
lib/lp/registry/browser/product.py (+435/-8)
lib/lp/registry/browser/productseries.py (+14/-315)
lib/lp/registry/browser/tests/test_product.py (+42/-0)
lib/lp/registry/browser/tests/test_productseries_views.py (+16/-0)
lib/lp/registry/interfaces/product.py (+7/-0)
lib/lp/registry/model/product.py (+17/-0)
lib/lp/registry/stories/product/xx-product-development-focus.txt (+2/-1)
lib/lp/registry/templates/product-index.pt (+8/-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/project-meta-go-import
Reviewer Review Type Date Requested Status
Launchpad code reviewers 2015-06-16 Pending
Review via email: mp+262144@code.launchpad.net

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

Commit Message

Add a go-lang remote import meta tag for git and bzr support.

Description of the Change

Provides a meta tag with default git repository and bzr branch metadata on Project +index/ProductSeries +index for golang's `go get`.

See: https://golang.org/cmd/go/#hdr-Remote_import_paths

To post a comment you must log in.

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-21 22:07:50 +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-21 22:07:50 +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-21 22:07:50 +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+.note {
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-21 22:07:50 +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-21 22:07:50 +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-21 22:07:50 +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-12 08:08:00 +0000
409+++ lib/lp/code/browser/configure.zcml 2015-06-21 22:07:50 +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-21 22:07:50 +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/stories/codeimport/xx-create-codeimport.txt'
518--- lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-15 05:28:12 +0000
519+++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2015-06-21 22:07:50 +0000
520@@ -38,11 +38,12 @@
521 >>> owner_browser = setupBrowser(auth="Basic test@canonical.com:test")
522 >>> owner_browser.open('http://code.launchpad.dev/firefox')
523 >>> owner_browser.getLink('Configure Code').click()
524+ >>> owner_browser.getControl('Bazaar', index=0).click()
525 >>> owner_browser.getControl(
526 ... 'Import a branch').click()
527 >>> owner_browser.getControl('Branch URL').value = (
528 ... 'git://example.com/firefox')
529- >>> owner_browser.getControl('Git').click()
530+ >>> owner_browser.getControl('Git', index=1).click()
531 >>> owner_browser.getControl('Branch name').value = 'trunk'
532 >>> owner_browser.getControl('Update').click()
533
534
535=== added file 'lib/lp/code/templates/configure-code-macros.pt'
536--- lib/lp/code/templates/configure-code-macros.pt 1970-01-01 00:00:00 +0000
537+++ lib/lp/code/templates/configure-code-macros.pt 2015-06-21 22:07:50 +0000
538@@ -0,0 +1,39 @@
539+<tal:root
540+ xmlns:tal="http://xml.zope.org/namespaces/tal"
541+ xmlns:metal="http://xml.zope.org/namespaces/metal"
542+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
543+ omit-tag="">
544+
545+ <div metal:define-macro="push-instructions-bzr" id="push-instructions-bzr" class="scm-tip">
546+ <h3>Push a new branch</h3>
547+ <p>You can push a Bazaar branch directly to Launchpad with the command:</p>
548+ <p>
549+ <code class="command command-block">
550+ bzr push lp:<tal:project replace="context/name"/>
551+ </code>
552+ </p>
553+ </div>
554+
555+ <div metal:define-macro="push-instructions-git" id="push-instructions-git" class="scm-tip">
556+ <h3>Push a new repository</h3>
557+ <p>You can add a remote for your Git branch with the
558+ command:</p>
559+ <p>
560+ <code class="command command-block">
561+ git remote add origin <tal:project replace="modules/lp.services.config/config/codehosting/git_ssh_root"/><tal:project replace="context/name"/><br />
562+ </code></p>
563+ <p>&hellip; and push the git branch to Launchpad with:</p>
564+ <p>
565+ <code class="command command-block">
566+ git push origin master
567+ </code>
568+ </p>
569+ </div>
570+
571+ <div metal:define-macro="no-keys" condition="not:view/user/sshkeys">
572+ <p class="note">To authenticate with the Launchpad branch upload service, you need to
573+ <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
574+ register a SSH key</a>.</p>
575+ </div>
576+
577+</tal:root>
578
579=== added file 'lib/lp/code/templates/configure-code.pt'
580--- lib/lp/code/templates/configure-code.pt 1970-01-01 00:00:00 +0000
581+++ lib/lp/code/templates/configure-code.pt 2015-06-21 22:07:50 +0000
582@@ -0,0 +1,167 @@
583+<html
584+ xmlns="http://www.w3.org/1999/xhtml"
585+ xmlns:tal="http://xml.zope.org/namespaces/tal"
586+ xmlns:metal="http://xml.zope.org/namespaces/metal"
587+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
588+ metal:use-macro="view/macro:page/main_only"
589+ i18n:domain="launchpad">
590+
591+ <body>
592+
593+ <metal:block fill-slot="head_epilogue">
594+ <style type="text/css">
595+ .subordinate {
596+ margin: 0.5em 0 0.5em 4em;
597+ }
598+ </style>
599+ </metal:block>
600+
601+ <div metal:fill-slot="main">
602+
603+ <div metal:use-macro="context/@@launchpad_form/form">
604+
605+ <metal:formbody fill-slot="widgets">
606+ <tal:block condition="not: view/is_series">
607+ <h3>Version control system</h3>
608+ <div id="default_vcs">
609+ <ul>
610+ <li>
611+ <label tal:replace="structure view/default_vcs_bzr">
612+ Bazaar
613+ </label>
614+ </li>
615+ <li>
616+ <label tal:replace="structure view/default_vcs_git">
617+ Git
618+ </label> (beta)
619+ </li>
620+ </ul>
621+ <p>
622+ <span class="note">Your project may have both
623+ Git repositories and Bazaar branches.</span><br/>
624+ </p>
625+ </div>
626+ </tal:block>
627+
628+ <div id="show-hide-bzr">
629+ <a href="#" id="bzr-expander-icon" class="expander-icon js-action">
630+ <span class="sprite branch">Bazaar settings</span>
631+ </a>
632+ <div id="bzr-expander-content">
633+ <div class="push-instructions">
634+ <div metal:use-macro="context/@@+configure-code-macros/push-instructions-bzr"></div>
635+ <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
636+ </div>
637+
638+ <table id="form_bzr" class="form">
639+ <tr>
640+ <td>
641+ <h3>Link or Import an existing branch</h3>
642+ <label tal:replace="structure view/branch_type_link">
643+ Link to a Bazaar branch already in Launchpad
644+ </label>
645+ <table class="subordinate">
646+ <tal:widget define="widget nocall:view/widgets/branch_location">
647+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
648+ </tal:widget>
649+ </table>
650+ </td>
651+ </tr>
652+
653+ <tr id="branch_mirror">
654+ <td>
655+ <label tal:replace="structure view/branch_type_import">
656+ Import a branch hosted somewhere else
657+ </label>
658+ <table class="subordinate">
659+ <tal:widget define="widget nocall:view/widgets/branch_name">
660+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
661+ </tal:widget>
662+ <tal:widget define="widget nocall:view/widgets/branch_owner">
663+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
664+ </tal:widget>
665+
666+ <tal:widget define="widget nocall:view/widgets/repo_url">
667+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
668+ </tal:widget>
669+
670+ <tr>
671+ <td>
672+ <label tal:replace="structure view/rcs_type_bzr">
673+ Bazaar, hosted externally
674+ </label>
675+ </td>
676+ </tr>
677+
678+ <tr>
679+ <td>
680+ <label tal:replace="structure view/rcs_type_git">
681+ Git
682+ </label>
683+ </td>
684+ </tr>
685+
686+ <tr>
687+ <td>
688+ <label tal:replace="structure view/rcs_type_svn">
689+ SVN
690+ </label>
691+ </td>
692+ </tr>
693+
694+ <tr>
695+ <td>
696+ <label tal:replace="structure view/rcs_type_cvs">
697+ CVS
698+ </label>
699+ <table class="subordinate">
700+ <tal:widget define="widget nocall:view/widgets/cvs_module">
701+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
702+ </tal:widget>
703+ </table>
704+ </td>
705+ </tr>
706+ </table>
707+ </td>
708+ </tr>
709+ </table>
710+ </div>
711+ <input tal:replace="structure view/rcs_type_emptymarker" />
712+ </div>
713+
714+ <tal:block condition="not: view/is_series">
715+ <div id="show-hide-git">
716+ <a href="#" id="git-expander-icon" class="expander-icon js-action">
717+ <span class="sprite gitbranch">Git settings</span>
718+ </a>
719+ <div id="git-expander-content">
720+ <div id="form_git" class="form">
721+ <span class="note">Git support is currently in beta.</span>
722+ <div class="push-instructions">
723+ <div metal:use-macro="context/@@+configure-code-macros/push-instructions-git"></div>
724+ <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
725+ </div>
726+
727+ <h3>Link an existing repository</h3>
728+ <p>Link to an existing Git repository already in Launchpad</p>
729+ <tal:widget define="widget nocall:view/widgets/git_repository_location">
730+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
731+ </tal:widget>
732+ </div>
733+ </div>
734+ </div>
735+ </tal:block>
736+
737+
738+ </metal:formbody>
739+ </div>
740+
741+ <script type="text/javascript">
742+ LPJS.use('lp.code.productseries_setbranch', function(Y) {
743+ Y.on('domready', Y.lp.code.productseries_setbranch.setup);
744+ });
745+ </script>
746+
747+ </div>
748+ </body>
749+</html>
750
751=== modified file 'lib/lp/code/templates/product-branch-summary.pt'
752--- lib/lp/code/templates/product-branch-summary.pt 2015-06-04 06:54:09 +0000
753+++ lib/lp/code/templates/product-branch-summary.pt 2015-06-21 22:07:50 +0000
754@@ -56,8 +56,7 @@
755 </p>
756 </div>
757
758- <tal:no-branches
759- condition="not: view/branch_count">
760+ <tal:no-branches condition="not: view/branch_count">
761 There are no branches for <tal:project-name replace="context/displayname"/>
762 in Launchpad.
763 <tal:can-configure condition="view/can_configure_branches">
764
765=== modified file 'lib/lp/registry/browser/configure.zcml'
766--- lib/lp/registry/browser/configure.zcml 2015-06-10 10:19:25 +0000
767+++ lib/lp/registry/browser/configure.zcml 2015-06-21 22:07:50 +0000
768@@ -2002,13 +2002,6 @@
769 template="../templates/milestone-add.pt"
770 />
771 <browser:page
772- name="+setbranch"
773- for="lp.registry.interfaces.productseries.IProductSeries"
774- class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
775- permission="launchpad.Edit"
776- template="../templates/productseries-setbranch.pt"
777- />
778- <browser:page
779 name="+review"
780 for="lp.registry.interfaces.productseries.IProductSeries"
781 class="lp.registry.browser.productseries.ProductSeriesReviewView"
782
783=== modified file 'lib/lp/registry/browser/product.py'
784--- lib/lp/registry/browser/product.py 2015-06-15 05:28:12 +0000
785+++ lib/lp/registry/browser/product.py 2015-06-21 22:07:50 +0000
786@@ -1,4 +1,4 @@
787-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
788+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
789 # GNU Affero General Public License version 3 (see the file LICENSE).
790
791 """Browser views for products."""
792@@ -30,6 +30,7 @@
793 'ProductRdfView',
794 'ProductReviewLicenseView',
795 'ProductSeriesSetView',
796+ 'ProductSetBranchView',
797 'ProductSetBreadcrumb',
798 'ProductSetNavigation',
799 'ProductSetReviewLicensesView',
800@@ -44,8 +45,12 @@
801
802 from operator import attrgetter
803
804+from bzrlib.revision import NULL_REVISION
805 from lazr.delegates import delegates
806-from lazr.restful.interface import copy_field
807+from lazr.restful.interface import (
808+ copy_field,
809+ use_template,
810+ )
811 from lazr.restful.interfaces import IJSONRequestCache
812 from z3c.ptcompat import ViewPageTemplateFile
813 from zope.component import getUtility
814@@ -66,6 +71,7 @@
815 from zope.schema import (
816 Bool,
817 Choice,
818+ TextLine,
819 )
820 from zope.schema.vocabulary import (
821 SimpleTerm,
822@@ -80,6 +86,7 @@
823 custom_widget,
824 LaunchpadEditFormView,
825 LaunchpadFormView,
826+ render_radio_widget_part,
827 ReturnToReferrerMixin,
828 safe_action,
829 )
830@@ -103,7 +110,10 @@
831 PUBLIC_PROPRIETARY_INFORMATION_TYPES,
832 ServiceUsage,
833 )
834-from lp.app.errors import NotFoundError
835+from lp.app.errors import (
836+ NotFoundError,
837+ UnexpectedFormData,
838+ )
839 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
840 from lp.app.utilities import json_dump_information_types
841 from lp.app.vocabularies import InformationTypeVocabulary
842@@ -131,9 +141,31 @@
843 StructuralSubscriptionTargetTraversalMixin,
844 )
845 from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
846+from lp.code.browser.branch import BranchNameValidationMixin
847 from lp.code.browser.branchref import BranchRef
848+from lp.code.browser.codeimport import validate_import_url
849 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
850+from lp.code.enums import (
851+ BranchType,
852+ RevisionControlSystems,
853+ )
854+from lp.code.errors import (
855+ BranchCreationForbidden,
856+ BranchExists,
857+ GitTargetError,
858+ )
859+from lp.code.interfaces.branch import IBranch
860+from lp.code.interfaces.branchcollection import IBranchCollection
861+from lp.code.interfaces.branchjob import IRosettaUploadJobSource
862+from lp.code.interfaces.branchtarget import IBranchTarget
863+from lp.code.interfaces.codeimport import (
864+ ICodeImport,
865+ ICodeImportSet,
866+ )
867+from lp.code.interfaces.gitcollection import IGitCollection
868+from lp.code.interfaces.gitrepository import IGitRepositorySet
869 from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
870+from lp.code.interfaces.gitrepository import IGitRepositorySet
871 from lp.registry.browser import (
872 add_subscribe_link,
873 BaseRdfView,
874@@ -150,6 +182,7 @@
875 PillarNavigationMixin,
876 PillarViewMixin,
877 )
878+from lp.registry.enums import VCSType
879 from lp.registry.interfaces.pillar import IPillarNameSet
880 from lp.registry.interfaces.product import (
881 IProduct,
882@@ -171,6 +204,7 @@
883 from lp.services.fields import (
884 PillarAliases,
885 PublicPersonChoice,
886+ URIField,
887 )
888 from lp.services.librarian.interfaces import ILibraryFileAliasSet
889 from lp.services.propertycache import cachedproperty
890@@ -187,6 +221,7 @@
891 stepto,
892 structured,
893 )
894+from lp.services.config import config
895 from lp.services.webapp.authorization import check_permission
896 from lp.services.webapp.batching import BatchNavigator
897 from lp.services.webapp.breadcrumb import Breadcrumb
898@@ -347,7 +382,6 @@
899 'configured' -- a boolean representing the configuration status.
900 """
901 overview_menu = MenuAPI(self.context).overview
902- series_menu = MenuAPI(self.context.development_focus).overview
903 configuration_names = [
904 'configure_bugtracker',
905 'configure_translations',
906@@ -363,11 +397,11 @@
907 configured=config_statuses[key]))
908
909 # Add the branch configuration in separately.
910- set_branch = series_menu['set_branch']
911- set_branch.text = 'Code'
912- set_branch.summary = "Specify the location of this project's code."
913+ configure_code = overview_menu['configure_code']
914+ configure_code.text = 'Code'
915+ configure_code.summary = "Specify the location of this project's code."
916 config_list.insert(0,
917- dict(link=set_branch,
918+ dict(link=configure_code,
919 configured=config_statuses['configure_codehosting']))
920 return config_list
921
922@@ -506,6 +540,7 @@
923 'packages',
924 'series',
925 'series_add',
926+ 'configure_code',
927 'milestones',
928 'downloads',
929 'announce',
930@@ -559,6 +594,14 @@
931 'RDF</abbr> metadata')
932 return Link('+rdf', text, icon='download')
933
934+ @enabled_with_permission('launchpad.Edit')
935+ def configure_code(self):
936+ """Return a link to configure code for this project."""
937+ text = 'Configure code'
938+ icon = 'edit'
939+ summary = 'Configure code for this project'
940+ return Link('+configure-code', text, summary, icon=icon)
941+
942 def downloads(self):
943 text = 'Downloads'
944 return Link('+download', text, icon='info')
945@@ -984,6 +1027,29 @@
946 def requestCountry(self):
947 return ICountry(self.request, None)
948
949+ @property
950+ def golang_import_spec(self):
951+ """Meta string for golang remote import path.
952+ See: https://golang.org/cmd/go/#hdr-Remote_import_paths
953+ """
954+ if self.context.vcs == VCSType.GIT:
955+ repo = getUtility(IGitRepositorySet).getDefaultRepository(
956+ self.context)
957+ if repo:
958+ return "{base_url}/{product} git {git_https_url}".format(
959+ base_url=config.launchpad.non_restricted_hostname,
960+ product=self.context.name,
961+ git_https_url=repo.git_https_url)
962+ else:
963+ return None
964+ elif self.context.vcs == VCSType.BZR:
965+ return "{base_url}/{product} bzr {codebrowse_root}{product}".format(
966+ base_url=config.launchpad.non_restricted_hostname,
967+ product=self.context.name,
968+ codebrowse_root=config.codehosting.secure_codebrowse_root)
969+ else:
970+ return None
971+
972 def browserLanguages(self):
973 return browser_languages(self.request)
974
975@@ -1594,6 +1660,367 @@
976 return BatchNavigator(decorated_result, self.request)
977
978
979+LINK_LP_BZR = 'link-lp-bzr'
980+IMPORT_EXTERNAL = 'import-external'
981+
982+
983+BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
984+ SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
985+ _("Link to a Bazaar branch already on Launchpad")),
986+ SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
987+ _("Import a branch hosted somewhere else")),
988+ ))
989+
990+class SetBranchForm(Interface):
991+ """The fields presented on the form for setting a branch."""
992+
993+ use_template(ICodeImport, ['cvs_module'])
994+
995+ default_vcs = Choice(title=_("Project VCS"),
996+ required=True, vocabulary=VCSType,
997+ description=_("The version control system for this project."))
998+
999+ rcs_type = Choice(title=_("Type of RCS"),
1000+ required=False, vocabulary=RevisionControlSystems,
1001+ description=_(
1002+ "The version control system to import from. "))
1003+
1004+ repo_url = URIField(
1005+ title=_("Branch URL"), required=True,
1006+ description=_("The URL of the branch."),
1007+ allowed_schemes=["http", "https"],
1008+ allow_userinfo=False, allow_port=True, allow_query=False,
1009+ allow_fragment=False, trailing_slash=False)
1010+
1011+ branch_location = copy_field(
1012+ IProductSeries['branch'], __name__='branch_location',
1013+ title=_('Branch'),
1014+ description=_(
1015+ "The Bazaar branch for this series in Launchpad, "
1016+ "if one exists."))
1017+
1018+ git_repository_location = TextLine(
1019+ title=_(
1020+ 'Git Repository'),
1021+ required=False,
1022+ description=_(
1023+ "The Git repository for this project in Launchpad, "
1024+ "if one exists, in the form: "
1025+ "~user/project-name/+git/repo-name"))
1026+
1027+ branch_type = Choice(
1028+ title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
1029+ description=_("The type of import"), required=True)
1030+
1031+ branch_name = copy_field(
1032+ IBranch['name'], __name__='branch_name', title=_('Branch name'),
1033+ description=_(''), required=True)
1034+
1035+ branch_owner = copy_field(
1036+ IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
1037+ description=_(''), required=True)
1038+
1039+
1040+class ProductSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
1041+ ProductView,
1042+ BranchNameValidationMixin):
1043+ """The view to set a branch default for the Product."""
1044+
1045+ label = 'Configure code'
1046+ page_title = label
1047+ schema = SetBranchForm
1048+ # Set for_input to True to ensure fields marked read-only will be editable
1049+ # upon creation.
1050+ for_input = True
1051+
1052+ custom_widget('rcs_type', LaunchpadRadioWidget)
1053+ custom_widget('branch_type', LaunchpadRadioWidget)
1054+ custom_widget('default_vcs', LaunchpadRadioWidget)
1055+
1056+ errors_in_action = False
1057+
1058+ @property
1059+ def is_series(self):
1060+ return False
1061+
1062+ @property
1063+ def series(self):
1064+ return self.context.development_focus
1065+
1066+ @property
1067+ def initial_values(self):
1068+ return dict(
1069+ rcs_type=RevisionControlSystems.BZR,
1070+ default_vcs=(self.context.pillar.inferred_vcs or VCSType.BZR),
1071+ branch_type=LINK_LP_BZR,
1072+ branch_location=self.series.branch)
1073+
1074+ @property
1075+ def next_url(self):
1076+ """Return the next_url.
1077+
1078+ Use the value from `ReturnToReferrerMixin` or None if there
1079+ are errors.
1080+ """
1081+ if self.errors_in_action:
1082+ return None
1083+ return super(ProductSetBranchView, self).next_url
1084+
1085+ def setUpWidgets(self):
1086+ """See `LaunchpadFormView`."""
1087+ super(ProductSetBranchView, self).setUpWidgets()
1088+ widget = self.widgets['rcs_type']
1089+ vocab = widget.vocabulary
1090+ current_value = widget._getFormValue()
1091+ self.rcs_type_cvs = render_radio_widget_part(
1092+ widget, vocab.CVS, current_value, 'CVS')
1093+ self.rcs_type_svn = render_radio_widget_part(
1094+ widget, vocab.BZR_SVN, current_value, 'SVN')
1095+ self.rcs_type_git = render_radio_widget_part(
1096+ widget, vocab.GIT, current_value)
1097+ self.rcs_type_bzr = render_radio_widget_part(
1098+ widget, vocab.BZR, current_value)
1099+ self.rcs_type_emptymarker = widget._emptyMarker()
1100+
1101+ widget = self.widgets['branch_type']
1102+ current_value = widget._getFormValue()
1103+ vocab = widget.vocabulary
1104+
1105+ (self.branch_type_link,
1106+ self.branch_type_import) = [
1107+ render_radio_widget_part(widget, value, current_value)
1108+ for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
1109+
1110+ widget = self.widgets['default_vcs']
1111+ vocab = widget.vocabulary
1112+ current_value = widget._getFormValue()
1113+ self.default_vcs_git = render_radio_widget_part(
1114+ widget, vocab.GIT, current_value, 'Git')
1115+ self.default_vcs_bzr = render_radio_widget_part(
1116+ widget, vocab.BZR, current_value, 'Bazaar')
1117+
1118+ def _validateLinkLpBzr(self, data):
1119+ """Validate data for link-lp-bzr case."""
1120+ if 'branch_location' not in data:
1121+ self.setFieldError(
1122+ 'branch_location', 'The branch location must be set.')
1123+
1124+ def _validateLinkLpGit(self, data):
1125+ """Validate data for link-lp-git case."""
1126+ if data.get('git_repository_location'):
1127+ repo = getUtility(IGitRepositorySet).getByPath(
1128+ self.user, data.get('git_repository_location'))
1129+ if not repo:
1130+ self.setFieldError(
1131+ 'git_repository_location',
1132+ 'The repository does not exist.')
1133+ if not (self.context == repo.target):
1134+ self.setFieldError(
1135+ 'git_repository_location',
1136+ 'The repository is in a different project.')
1137+
1138+ def _validateImportExternal(self, data):
1139+ """Validate data for import external case."""
1140+ rcs_type = data.get('rcs_type')
1141+ repo_url = data.get('repo_url')
1142+
1143+ # Private teams are forbidden from owning code imports.
1144+ branch_owner = data.get('branch_owner')
1145+ if branch_owner is not None and branch_owner.private:
1146+ self.setFieldError(
1147+ 'branch_owner', 'Private teams are forbidden from owning '
1148+ 'external imports.')
1149+
1150+ if repo_url is None:
1151+ self.setFieldError(
1152+ 'repo_url', 'You must set the external repository URL.')
1153+ else:
1154+ reason = validate_import_url(repo_url)
1155+ if reason:
1156+ self.setFieldError('repo_url', reason)
1157+
1158+ # RCS type is mandatory.
1159+ # This condition should never happen since an initial value is set.
1160+ if rcs_type is None:
1161+ # The error shows but does not identify the widget.
1162+ self.setFieldError(
1163+ 'rcs_type',
1164+ 'You must specify the type of RCS for the remote host.')
1165+ elif rcs_type == RevisionControlSystems.CVS:
1166+ if 'cvs_module' not in data:
1167+ self.setFieldError('cvs_module', 'The CVS module must be set.')
1168+ self._validateBranch(data)
1169+
1170+ def _validateBranch(self, data):
1171+ """Validate that branch name and owner are set."""
1172+ if 'branch_name' not in data:
1173+ self.setFieldError('branch_name', 'The branch name must be set.')
1174+ if 'branch_owner' not in data:
1175+ self.setFieldError('branch_owner', 'The branch owner must be set.')
1176+
1177+ def _setRequired(self, names, value):
1178+ """Mark the widget field as optional."""
1179+ for name in names:
1180+ widget = self.widgets[name]
1181+ # The 'required' property on the widget context is set to False.
1182+ # The widget also has a 'required' property but it isn't used
1183+ # during validation.
1184+ widget.context.required = value
1185+
1186+ def _validSchemes(self, rcs_type):
1187+ """Return the valid schemes for the repository URL."""
1188+ schemes = set(['http', 'https'])
1189+ # Extend the allowed schemes for the repository URL based on
1190+ # rcs_type.
1191+ extra_schemes = {
1192+ RevisionControlSystems.BZR_SVN: ['svn'],
1193+ RevisionControlSystems.GIT: ['git'],
1194+ RevisionControlSystems.BZR: ['bzr'],
1195+ }
1196+ schemes.update(extra_schemes.get(rcs_type, []))
1197+ return schemes
1198+
1199+ def validate_widgets(self, data, names=None):
1200+ """See `LaunchpadFormView`."""
1201+ names = ['branch_type', 'rcs_type', 'default_vcs']
1202+ super(ProductSetBranchView, self).validate_widgets(data, names)
1203+ branch_type = data.get('branch_type')
1204+
1205+ if branch_type == LINK_LP_BZR:
1206+ # Mark other widgets as non-required.
1207+ self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
1208+ 'branch_name', 'branch_owner'], False)
1209+ elif branch_type == IMPORT_EXTERNAL:
1210+ rcs_type = data.get('rcs_type')
1211+
1212+ # Set the valid schemes based on rcs_type.
1213+ self.widgets['repo_url'].field.allowed_schemes = (
1214+ self._validSchemes(rcs_type))
1215+ # The branch location is not required for validation.
1216+ self._setRequired(['branch_location'], False)
1217+ # The cvs_module is required if it is a CVS import.
1218+ if rcs_type == RevisionControlSystems.CVS:
1219+ self._setRequired(['cvs_module'], True)
1220+ else:
1221+ raise AssertionError("Unknown branch type %s" % branch_type)
1222+ # Perform full validation now.
1223+ super(ProductSetBranchView, self).validate_widgets(data)
1224+
1225+ def validate(self, data):
1226+ """See `LaunchpadFormView`."""
1227+ # If widget validation returned errors then there is no need to
1228+ # continue as we'd likely just override the errors reported there.
1229+ if len(self.errors) > 0:
1230+ return
1231+ branch_type = data.get('branch_type')
1232+ self._validateLinkLpGit(data)
1233+ if branch_type == IMPORT_EXTERNAL:
1234+ self._validateImportExternal(data)
1235+ elif branch_type == LINK_LP_BZR:
1236+ self._validateLinkLpBzr(data)
1237+ else:
1238+ raise AssertionError("Unknown branch type %s" % branch_type)
1239+
1240+ @property
1241+ def target(self):
1242+ """The branch target for the context."""
1243+ return IBranchTarget(self.context)
1244+
1245+ def abort_update():
1246+ """Abort transaction.
1247+
1248+ This is normally handled by LaunchpadFormView, but this can be called
1249+ from the success handler."""
1250+
1251+ self.errors_in_action = True
1252+ self._abort()
1253+ return
1254+
1255+ def add_update_notification(self):
1256+ self.request.response.addInfoNotification(
1257+ 'Project code updated.')
1258+
1259+ @action(_('Update'), name='update')
1260+ def update_action(self, action, data):
1261+ branch_type = data.get('branch_type')
1262+ default_vcs = data.get('default_vcs')
1263+
1264+ if default_vcs:
1265+ self.context.vcs = default_vcs
1266+
1267+ git_repository_location = data.get('git_repository_location')
1268+ if git_repository_location:
1269+ repo = getUtility(IGitRepositorySet).getByPath(
1270+ self.user, git_repository_location)
1271+ if branch_type == LINK_LP_BZR:
1272+ branch_location = data.get('branch_location')
1273+ if branch_location != self.series.branch:
1274+ self.series.branch = branch_location
1275+ # Request an initial upload of translation files.
1276+ getUtility(IRosettaUploadJobSource).create(
1277+ self.series.branch, NULL_REVISION)
1278+ else:
1279+ self.series.branch = branch_location
1280+ self.add_update_notification()
1281+ else:
1282+ branch_name = data.get('branch_name')
1283+ branch_owner = data.get('branch_owner')
1284+
1285+ if branch_type == IMPORT_EXTERNAL:
1286+ rcs_type = data.get('rcs_type')
1287+ if rcs_type == RevisionControlSystems.CVS:
1288+ cvs_root = data.get('repo_url')
1289+ cvs_module = data.get('cvs_module')
1290+ url = None
1291+ else:
1292+ cvs_root = None
1293+ cvs_module = None
1294+ url = data.get('repo_url')
1295+ rcs_item = RevisionControlSystems.items[rcs_type.name]
1296+ try:
1297+ code_import = getUtility(ICodeImportSet).new(
1298+ owner=branch_owner,
1299+ registrant=self.user,
1300+ target=IBranchTarget(self.target),
1301+ branch_name=branch_name,
1302+ rcs_type=rcs_item,
1303+ url=url,
1304+ cvs_root=cvs_root,
1305+ cvs_module=cvs_module)
1306+ except BranchExists as e:
1307+ self._setBranchExists(e.existing_branch, 'branch_name')
1308+ abort_update()
1309+ self.series.branch = code_import.branch
1310+ self.request.response.addInfoNotification(
1311+ 'Code import created and branch linked to the series.')
1312+ else:
1313+ raise UnexpectedFormData(branch_type)
1314+
1315+ def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
1316+ """Create a new hosted Bazaar branch.
1317+
1318+ Return the branch on success or None.
1319+ """
1320+ branch = None
1321+ try:
1322+ namespace = self.target.getNamespace(branch_owner)
1323+ branch = namespace.createBranch(
1324+ branch_type=BranchType.HOSTED, name=branch_name,
1325+ registrant=self.user, url=repo_url)
1326+ except BranchCreationForbidden:
1327+ self.addError(
1328+ "You are not allowed to create branches in %s." %
1329+ self.context.displayname)
1330+ except BranchExists as e:
1331+ self._setBranchExists(e.existing_branch, 'branch_name')
1332+ if branch is None:
1333+ self.errors_in_action = True
1334+ # Abort transaction. This is normally handled by
1335+ # LaunchpadFormView, but we are already in the success handler.
1336+ self._abort()
1337+ return branch
1338+
1339+
1340 class ProductRdfView(BaseRdfView):
1341 """A view that sets its mime-type to application/rdf+xml"""
1342
1343
1344=== modified file 'lib/lp/registry/browser/productseries.py'
1345--- lib/lp/registry/browser/productseries.py 2015-06-12 14:20:12 +0000
1346+++ lib/lp/registry/browser/productseries.py 2015-06-21 22:07:50 +0000
1347@@ -28,11 +28,6 @@
1348
1349 from operator import attrgetter
1350
1351-from bzrlib.revision import NULL_REVISION
1352-from lazr.restful.interface import (
1353- copy_field,
1354- use_template,
1355- )
1356 from z3c.ptcompat import ViewPageTemplateFile
1357 from zope.component import getUtility
1358 from zope.formlib import form
1359@@ -57,17 +52,11 @@
1360 custom_widget,
1361 LaunchpadEditFormView,
1362 LaunchpadFormView,
1363- render_radio_widget_part,
1364- ReturnToReferrerMixin,
1365 )
1366 from lp.app.browser.tales import MenuAPI
1367 from lp.app.enums import ServiceUsage
1368-from lp.app.errors import (
1369- NotFoundError,
1370- UnexpectedFormData,
1371- )
1372+from lp.app.errors import NotFoundError
1373 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1374-from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
1375 from lp.app.widgets.textwidgets import StrippedTextWidget
1376 from lp.blueprints.browser.specificationtarget import (
1377 HasSpecificationsMenuMixin,
1378@@ -81,24 +70,9 @@
1379 StructuralSubscriptionTargetTraversalMixin,
1380 )
1381 from lp.bugs.interfaces.bugtask import IBugTaskSet
1382-from lp.code.browser.branch import BranchNameValidationMixin
1383 from lp.code.browser.branchref import BranchRef
1384-from lp.code.browser.codeimport import validate_import_url
1385-from lp.code.enums import (
1386- BranchType,
1387- RevisionControlSystems,
1388- )
1389-from lp.code.errors import (
1390- BranchCreationForbidden,
1391- BranchExists,
1392- )
1393-from lp.code.interfaces.branch import IBranch
1394-from lp.code.interfaces.branchjob import IRosettaUploadJobSource
1395+from lp.code.enums import RevisionControlSystems
1396 from lp.code.interfaces.branchtarget import IBranchTarget
1397-from lp.code.interfaces.codeimport import (
1398- ICodeImport,
1399- ICodeImportSet,
1400- )
1401 from lp.registry.browser import (
1402 add_subscribe_link,
1403 BaseRdfView,
1404@@ -110,6 +84,7 @@
1405 InvolvedMenu,
1406 PillarInvolvementView,
1407 )
1408+from lp.registry.browser.product import ProductSetBranchView
1409 from lp.registry.errors import CannotPackageProprietaryProduct
1410 from lp.registry.interfaces.packaging import (
1411 IPackaging,
1412@@ -117,7 +92,6 @@
1413 )
1414 from lp.registry.interfaces.productseries import IProductSeries
1415 from lp.registry.interfaces.series import SeriesStatus
1416-from lp.services.fields import URIField
1417 from lp.services.propertycache import cachedproperty
1418 from lp.services.webapp import (
1419 ApplicationMenu,
1420@@ -756,300 +730,25 @@
1421 self.next_url = canonical_url(product)
1422
1423
1424-LINK_LP_BZR = 'link-lp-bzr'
1425-IMPORT_EXTERNAL = 'import-external'
1426-
1427-
1428-BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
1429- SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
1430- _("Link to a Bazaar branch already on Launchpad")),
1431- SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
1432- _("Import a branch hosted somewhere else")),
1433- ))
1434-
1435-
1436-class SetBranchForm(Interface):
1437- """The fields presented on the form for setting a branch."""
1438-
1439- use_template(ICodeImport, ['cvs_module'])
1440-
1441- rcs_type = Choice(title=_("Type of RCS"),
1442- required=False, vocabulary=RevisionControlSystems,
1443- description=_(
1444- "The version control system to import from. "))
1445-
1446- repo_url = URIField(
1447- title=_("Branch URL"), required=True,
1448- description=_("The URL of the branch."),
1449- allowed_schemes=["http", "https"],
1450- allow_userinfo=False, allow_port=True, allow_query=False,
1451- allow_fragment=False, trailing_slash=False)
1452-
1453- branch_location = copy_field(
1454- IProductSeries['branch'], __name__='branch_location',
1455- title=_('Branch'),
1456- description=_(
1457- "The Bazaar branch for this series in Launchpad, "
1458- "if one exists."))
1459-
1460- branch_type = Choice(
1461- title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
1462- description=_("The type of import"), required=True)
1463-
1464- branch_name = copy_field(
1465- IBranch['name'], __name__='branch_name', title=_('Branch name'),
1466- description=_(''), required=True)
1467-
1468- branch_owner = copy_field(
1469- IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
1470- description=_(''), required=True)
1471-
1472-
1473-class ProductSeriesSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
1474- ProductSeriesView,
1475- BranchNameValidationMixin):
1476+class ProductSeriesSetBranchView(ProductSetBranchView, ProductSeriesView):
1477 """The view to set a branch for the ProductSeries."""
1478
1479- schema = SetBranchForm
1480- # Set for_input to True to ensure fields marked read-only will be editable
1481- # upon creation.
1482- for_input = True
1483-
1484- custom_widget('rcs_type', LaunchpadRadioWidget)
1485- custom_widget('branch_type', LaunchpadRadioWidget)
1486-
1487- errors_in_action = False
1488-
1489- @property
1490- def initial_values(self):
1491- return dict(
1492- rcs_type=RevisionControlSystems.BZR,
1493- branch_type=LINK_LP_BZR,
1494- branch_location=self.context.branch)
1495-
1496- @property
1497- def next_url(self):
1498- """Return the next_url.
1499-
1500- Use the value from `ReturnToReferrerMixin` or None if there
1501- are errors.
1502- """
1503- if self.errors_in_action:
1504- return None
1505- return super(ProductSeriesSetBranchView, self).next_url
1506-
1507- def setUpWidgets(self):
1508- """See `LaunchpadFormView`."""
1509- super(ProductSeriesSetBranchView, self).setUpWidgets()
1510- widget = self.widgets['rcs_type']
1511- vocab = widget.vocabulary
1512- current_value = widget._getFormValue()
1513- self.rcs_type_cvs = render_radio_widget_part(
1514- widget, vocab.CVS, current_value, 'CVS')
1515- self.rcs_type_svn = render_radio_widget_part(
1516- widget, vocab.BZR_SVN, current_value, 'SVN')
1517- self.rcs_type_git = render_radio_widget_part(
1518- widget, vocab.GIT, current_value)
1519- self.rcs_type_bzr = render_radio_widget_part(
1520- widget, vocab.BZR, current_value)
1521- self.rcs_type_emptymarker = widget._emptyMarker()
1522-
1523- widget = self.widgets['branch_type']
1524- current_value = widget._getFormValue()
1525- vocab = widget.vocabulary
1526-
1527- (self.branch_type_link,
1528- self.branch_type_import) = [
1529- render_radio_widget_part(widget, value, current_value)
1530- for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
1531-
1532- def _validateLinkLpBzr(self, data):
1533- """Validate data for link-lp-bzr case."""
1534- if 'branch_location' not in data:
1535- self.setFieldError(
1536- 'branch_location', 'The branch location must be set.')
1537-
1538- def _validateImportExternal(self, data):
1539- """Validate data for import external case."""
1540- rcs_type = data.get('rcs_type')
1541- repo_url = data.get('repo_url')
1542-
1543- # Private teams are forbidden from owning code imports.
1544- branch_owner = data.get('branch_owner')
1545- if branch_owner is not None and branch_owner.private:
1546- self.setFieldError(
1547- 'branch_owner', 'Private teams are forbidden from owning '
1548- 'external imports.')
1549-
1550- if repo_url is None:
1551- self.setFieldError(
1552- 'repo_url', 'You must set the external repository URL.')
1553- else:
1554- reason = validate_import_url(repo_url, rcs_type)
1555- if reason:
1556- self.setFieldError('repo_url', reason)
1557-
1558- # RCS type is mandatory.
1559- # This condition should never happen since an initial value is set.
1560- if rcs_type is None:
1561- # The error shows but does not identify the widget.
1562- self.setFieldError(
1563- 'rcs_type',
1564- 'You must specify the type of RCS for the remote host.')
1565- elif rcs_type == RevisionControlSystems.CVS:
1566- if 'cvs_module' not in data:
1567- self.setFieldError('cvs_module', 'The CVS module must be set.')
1568- self._validateBranch(data)
1569-
1570- def _validateBranch(self, data):
1571- """Validate that branch name and owner are set."""
1572- if 'branch_name' not in data:
1573- self.setFieldError('branch_name', 'The branch name must be set.')
1574- if 'branch_owner' not in data:
1575- self.setFieldError('branch_owner', 'The branch owner must be set.')
1576-
1577- def _setRequired(self, names, value):
1578- """Mark the widget field as optional."""
1579- for name in names:
1580- widget = self.widgets[name]
1581- # The 'required' property on the widget context is set to False.
1582- # The widget also has a 'required' property but it isn't used
1583- # during validation.
1584- widget.context.required = value
1585-
1586- def _validSchemes(self, rcs_type):
1587- """Return the valid schemes for the repository URL."""
1588- schemes = set(['http', 'https'])
1589- # Extend the allowed schemes for the repository URL based on
1590- # rcs_type.
1591- extra_schemes = {
1592- RevisionControlSystems.BZR_SVN: ['svn'],
1593- RevisionControlSystems.GIT: ['git'],
1594- RevisionControlSystems.BZR: ['bzr'],
1595- }
1596- schemes.update(extra_schemes.get(rcs_type, []))
1597- return schemes
1598-
1599- def validate_widgets(self, data, names=None):
1600- """See `LaunchpadFormView`."""
1601- names = ['branch_type', 'rcs_type']
1602- super(ProductSeriesSetBranchView, self).validate_widgets(data, names)
1603- branch_type = data.get('branch_type')
1604- if branch_type == LINK_LP_BZR:
1605- # Mark other widgets as non-required.
1606- self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
1607- 'branch_name', 'branch_owner'], False)
1608- elif branch_type == IMPORT_EXTERNAL:
1609- rcs_type = data.get('rcs_type')
1610-
1611- # Set the valid schemes based on rcs_type.
1612- self.widgets['repo_url'].field.allowed_schemes = (
1613- self._validSchemes(rcs_type))
1614- # The branch location is not required for validation.
1615- self._setRequired(['branch_location'], False)
1616- # The cvs_module is required if it is a CVS import.
1617- if rcs_type == RevisionControlSystems.CVS:
1618- self._setRequired(['cvs_module'], True)
1619- else:
1620- raise AssertionError("Unknown branch type %s" % branch_type)
1621- # Perform full validation now.
1622- super(ProductSeriesSetBranchView, self).validate_widgets(data)
1623-
1624- def validate(self, data):
1625- """See `LaunchpadFormView`."""
1626- # If widget validation returned errors then there is no need to
1627- # continue as we'd likely just override the errors reported there.
1628- if len(self.errors) > 0:
1629- return
1630- branch_type = data['branch_type']
1631- if branch_type == IMPORT_EXTERNAL:
1632- self._validateImportExternal(data)
1633- elif branch_type == LINK_LP_BZR:
1634- self._validateLinkLpBzr(data)
1635- else:
1636- raise AssertionError("Unknown branch type %s" % branch_type)
1637+ @property
1638+ def is_series(self):
1639+ return True
1640+
1641+ @property
1642+ def series(self):
1643+ return self.context
1644
1645 @property
1646 def target(self):
1647 """The branch target for the context."""
1648 return IBranchTarget(self.context.product)
1649
1650- @action(_('Update'), name='update')
1651- def update_action(self, action, data):
1652- branch_type = data.get('branch_type')
1653- if branch_type == LINK_LP_BZR:
1654- branch_location = data.get('branch_location')
1655- if branch_location != self.context.branch:
1656- self.context.branch = branch_location
1657- # Request an initial upload of translation files.
1658- getUtility(IRosettaUploadJobSource).create(
1659- self.context.branch, NULL_REVISION)
1660- else:
1661- self.context.branch = branch_location
1662- self.request.response.addInfoNotification(
1663- 'Series code location updated.')
1664- else:
1665- branch_name = data.get('branch_name')
1666- branch_owner = data.get('branch_owner')
1667-
1668- if branch_type == IMPORT_EXTERNAL:
1669- rcs_type = data.get('rcs_type')
1670- if rcs_type == RevisionControlSystems.CVS:
1671- cvs_root = data.get('repo_url')
1672- cvs_module = data.get('cvs_module')
1673- url = None
1674- else:
1675- cvs_root = None
1676- cvs_module = None
1677- url = data.get('repo_url')
1678- rcs_item = RevisionControlSystems.items[rcs_type.name]
1679- try:
1680- code_import = getUtility(ICodeImportSet).new(
1681- owner=branch_owner,
1682- registrant=self.user,
1683- target=IBranchTarget(self.context.product),
1684- branch_name=branch_name,
1685- rcs_type=rcs_item,
1686- url=url,
1687- cvs_root=cvs_root,
1688- cvs_module=cvs_module)
1689- except BranchExists as e:
1690- self._setBranchExists(e.existing_branch, 'branch_name')
1691- self.errors_in_action = True
1692- # Abort transaction. This is normally handled
1693- # by LaunchpadFormView, but we are already in
1694- # the success handler.
1695- self._abort()
1696- return
1697- self.context.branch = code_import.branch
1698- self.request.response.addInfoNotification(
1699- 'Code import created and branch linked to the series.')
1700- else:
1701- raise UnexpectedFormData(branch_type)
1702-
1703- def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
1704- """Create a new hosted Bazaar branch.
1705-
1706- Return the branch on success or None.
1707- """
1708- branch = None
1709- try:
1710- namespace = self.target.getNamespace(branch_owner)
1711- branch = namespace.createBranch(
1712- branch_type=BranchType.HOSTED, name=branch_name,
1713- registrant=self.user, url=repo_url)
1714- except BranchCreationForbidden:
1715- self.addError(
1716- "You are not allowed to create branches in %s." %
1717- self.context.displayname)
1718- except BranchExists as e:
1719- self._setBranchExists(e.existing_branch, 'branch_name')
1720- if branch is None:
1721- self.errors_in_action = True
1722- # Abort transaction. This is normally handled by
1723- # LaunchpadFormView, but we are already in the success handler.
1724- self._abort()
1725- return branch
1726+ def add_update_notification(self):
1727+ self.request.response.addInfoNotification(
1728+ 'Series code location updated.')
1729
1730
1731 class ProductSeriesReviewView(LaunchpadEditFormView):
1732
1733=== modified file 'lib/lp/registry/browser/tests/test_product.py'
1734--- lib/lp/registry/browser/tests/test_product.py 2014-06-19 06:38:53 +0000
1735+++ lib/lp/registry/browser/tests/test_product.py 2015-06-21 22:07:50 +0000
1736@@ -27,6 +27,7 @@
1737 PROPRIETARY_INFORMATION_TYPES,
1738 ServiceUsage,
1739 )
1740+from lp.code.interfaces.gitrepository import IGitRepositorySet
1741 from lp.registry.browser.product import (
1742 ProjectAddStepOne,
1743 ProjectAddStepTwo,
1744@@ -34,6 +35,7 @@
1745 from lp.registry.enums import (
1746 EXCLUSIVE_TEAM_POLICY,
1747 TeamMembershipPolicy,
1748+ VCSType,
1749 )
1750 from lp.registry.interfaces.product import (
1751 IProductSet,
1752@@ -294,6 +296,40 @@
1753 super(TestProductView, self).setUp()
1754 self.product = self.factory.makeProduct(name='fnord')
1755
1756+ def test_golang_meta_renders_git(self):
1757+ # ensure golang meta import path is rendered if project has
1758+ # git default vcs.
1759+ # See: https://golang.org/cmd/go/#hdr-Remote_import_paths
1760+ repo = self.factory.makeGitRepository()
1761+ view = create_initialized_view(repo.target, '+index')
1762+ with person_logged_in(repo.target.owner):
1763+ getUtility(IGitRepositorySet).setDefaultRepository(
1764+ target=repo.target, repository=repo)
1765+ repo.target.vcs = VCSType.GIT
1766+
1767+ golang_import = '{base}/{product_name} git {repo_url}'.format(
1768+ base=config.launchpad.non_restricted_hostname,
1769+ product_name=repo.target.name,
1770+ repo_url=repo.git_https_url
1771+ )
1772+ self.assertEqual(golang_import, view.golang_import_spec)
1773+
1774+ def test_golang_meta_renders_bzr(self):
1775+ # ensure golang meta import path is rendered if project has
1776+ # bzr default vcs.
1777+ # See: https://golang.org/cmd/go/#hdr-Remote_import_paths
1778+ branch = self.factory.makeBranch()
1779+ view = create_initialized_view(branch.product, '+index')
1780+ with person_logged_in(branch.product.owner):
1781+ branch.product.vcs = VCSType.BZR
1782+
1783+ golang_import = '{base}/{name} bzr {repo_url}{name}'.format(
1784+ base=config.launchpad.non_restricted_hostname,
1785+ name=branch.target.name,
1786+ repo_url=config.codehosting.secure_codebrowse_root
1787+ )
1788+ self.assertEqual(golang_import, view.golang_import_spec)
1789+
1790 def test_show_programming_languages_without_languages(self):
1791 # show_programming_languages is false when there are no programming
1792 # languages set.
1793@@ -309,6 +345,12 @@
1794 view = create_initialized_view(self.product, '+index')
1795 self.assertTrue(view.show_programming_languages)
1796
1797+ def test_show_default_vcs(self):
1798+ with person_logged_in(self.product.owner):
1799+ self.product.vcs = VCSType.GIT
1800+ view = create_initialized_view(self.product, '+index')
1801+ self.assertTrue(view.show_vcs)
1802+
1803 def test_show_license_info_without_other_license(self):
1804 # show_license_info is false when one of the "other" licences is
1805 # not selected.
1806
1807=== modified file 'lib/lp/registry/browser/tests/test_productseries_views.py'
1808--- lib/lp/registry/browser/tests/test_productseries_views.py 2013-04-03 03:09:04 +0000
1809+++ lib/lp/registry/browser/tests/test_productseries_views.py 2015-06-21 22:07:50 +0000
1810@@ -30,6 +30,22 @@
1811
1812 layer = DatabaseFunctionalLayer
1813
1814+ def test_golang_meta_renders(self):
1815+ # ensure golang meta import path is rendered if project series has
1816+ # git default.
1817+ # See: https://golang.org/cmd/go/#hdr-Remote_import_paths
1818+ repo = self.factory.makeGitRepository()
1819+ view = create_initialized_view(repo.target, '+index')
1820+ with person_logged_in(repo.target.owner):
1821+ getUtility(IGitRepositorySet).setDefaultRepository(
1822+ target=repo.target, repository=repo)
1823+ golang_import = '{base}/{product_name} git {repo_url}'.format(
1824+ base=config.launchpad.non_restricted_hostname,
1825+ product_name=repo.target.name,
1826+ repo_url=repo.git_https_url
1827+ )
1828+ self.assertEqual(golang_import, view.golang_import_spec)
1829+
1830 def test_information_type_public(self):
1831 # A ProductSeries view should include its information_type,
1832 # which defaults to Public for new projects.
1833
1834=== modified file 'lib/lp/registry/interfaces/product.py'
1835--- lib/lp/registry/interfaces/product.py 2015-05-13 05:03:33 +0000
1836+++ lib/lp/registry/interfaces/product.py 2015-06-21 22:07:50 +0000
1837@@ -761,6 +761,13 @@
1838 description=_(
1839 "Version control system for this project's code.")))
1840
1841+ inferred_vcs = exported(
1842+ TextLine(
1843+ title=_("Inferred VCS"),
1844+ readonly=True,
1845+ description=_(
1846+ "Inferred version control system for this project's code.")))
1847+
1848 def getAllowedBugInformationTypes():
1849 """Get the information types that a bug in this project can have.
1850
1851
1852=== modified file 'lib/lp/registry/model/product.py'
1853--- lib/lp/registry/model/product.py 2015-05-13 05:25:30 +0000
1854+++ lib/lp/registry/model/product.py 2015-06-21 22:07:50 +0000
1855@@ -116,6 +116,8 @@
1856 )
1857 from lp.code.enums import BranchType
1858 from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
1859+from lp.code.interfaces.branchcollection import IBranchCollection
1860+from lp.code.interfaces.gitcollection import IGitCollection
1861 from lp.code.interfaces.gitrepository import IGitRepositorySet
1862 from lp.code.model.branch import Branch
1863 from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
1864@@ -445,6 +447,21 @@
1865 """
1866 return None
1867
1868+ @property
1869+ def inferred_vcs(self):
1870+ """Use vcs, otherwise infer from existence of git or bzr branches.
1871+
1872+ Bzr take precedence over git, if no project vcs set.
1873+ """
1874+ vcs = None
1875+ if self.vcs:
1876+ return self.vcs
1877+ if not IBranchCollection(self).is_empty():
1878+ vcs = VCSType.BZR
1879+ elif not IGitCollection(self).is_empty():
1880+ vcs = VCSType.GIT
1881+ return vcs
1882+
1883 @date_next_suggest_packaging.setter # pyflakes:ignore
1884 def date_next_suggest_packaging(self, value):
1885 """See `IProduct`
1886
1887=== modified file 'lib/lp/registry/stories/product/xx-product-development-focus.txt'
1888--- lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-14 23:48:24 +0000
1889+++ lib/lp/registry/stories/product/xx-product-development-focus.txt 2015-06-21 22:07:50 +0000
1890@@ -74,7 +74,7 @@
1891 (http://launchpad.dev/fooix/+edit)
1892 >>> print_involvement_portlet(owner_browser)
1893 Code
1894- http://launchpad.dev/fooix/trunk/+setbranch
1895+ http://launchpad.dev/fooix/+setbranch
1896 Bugs
1897 http://launchpad.dev/fooix/+configure-bugtracker
1898 Translations
1899@@ -85,6 +85,7 @@
1900 The owner can specify the development focus branch from the overview page.
1901
1902 >>> owner_browser.getLink(url='+setbranch').click()
1903+ >>> owner_browser.getControl('Bazaar', index=0).click()
1904 >>> owner_browser.getControl(name='field.branch_location').value = (
1905 ... '~eric/fooix/trunk')
1906 >>> owner_browser.getControl('Update').click()
1907
1908=== modified file 'lib/lp/registry/templates/product-index.pt'
1909--- lib/lp/registry/templates/product-index.pt 2015-01-29 16:28:30 +0000
1910+++ lib/lp/registry/templates/product-index.pt 2015-06-21 22:07:50 +0000
1911@@ -25,6 +25,9 @@
1912 });
1913 </script>
1914 </tal:uses_launchpad_bugtracker>
1915+
1916+ <meta name="go-import" tal:condition="view/golang_import_spec"
1917+ tal:attributes="content view/golang_import_spec" />
1918 </tal:head-epilogue>
1919 </head>
1920
1921@@ -129,6 +132,11 @@
1922 <dd tal:content="structure view/languages_edit_widget" />
1923 </dl>
1924
1925+ <dl id="product-vcs" tal:condition="context/inferred_vcs">
1926+ <dt>Version control system:</dt>
1927+ <dd tal:content="context/inferred_vcs/title">Git</dd>
1928+ </dl>
1929+
1930 <dl id="licences">
1931 <dt>Licences:</dt>
1932 <dd>
1933
1934=== removed file 'lib/lp/registry/templates/productseries-setbranch.pt'
1935--- lib/lp/registry/templates/productseries-setbranch.pt 2015-05-01 13:18:54 +0000
1936+++ lib/lp/registry/templates/productseries-setbranch.pt 1970-01-01 00:00:00 +0000
1937@@ -1,125 +0,0 @@
1938-<html
1939- xmlns="http://www.w3.org/1999/xhtml"
1940- xmlns:tal="http://xml.zope.org/namespaces/tal"
1941- xmlns:metal="http://xml.zope.org/namespaces/metal"
1942- xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1943- metal:use-macro="view/macro:page/main_only"
1944- i18n:domain="launchpad">
1945-
1946-<body>
1947-
1948-<metal:block fill-slot="head_epilogue">
1949- <style type="text/css">
1950- .subordinate {
1951- margin: 0.5em 0 0.5em 4em;
1952- }
1953- </style>
1954-</metal:block>
1955-
1956-<div metal:fill-slot="main">
1957-
1958- <p id="push-instructions">
1959- You can push a Bazaar branch directly to Launchpad with the command:<br />
1960- <tt class="command">
1961- bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/<tal:series replace="context/name"/>
1962- </tt>
1963- <tal:no-keys condition="not:view/user/sshkeys">
1964- <br/>To authenticate with the Launchpad branch upload service,
1965- you need to
1966- <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
1967- register a SSH key</a>.
1968- </tal:no-keys>
1969- </p>
1970-
1971- <div metal:use-macro="context/@@launchpad_form/form">
1972-
1973- <metal:formbody fill-slot="widgets">
1974-
1975- <table class="form">
1976-
1977- <tr>
1978- <td>
1979- <label tal:replace="structure view/branch_type_link">
1980- Link to a Bazaar branch already in Launchpad
1981- </label>
1982- <table class="subordinate">
1983- <tal:widget define="widget nocall:view/widgets/branch_location">
1984- <metal:block use-macro="context/@@launchpad_form/widget_row" />
1985- </tal:widget>
1986- </table>
1987- </td>
1988- </tr>
1989-
1990- <tr>
1991- <td>
1992- <label tal:replace="structure view/branch_type_import">
1993- Import a branch hosted somewhere else
1994- </label>
1995- <table class="subordinate">
1996- <tal:widget define="widget nocall:view/widgets/repo_url">
1997- <metal:block use-macro="context/@@launchpad_form/widget_row" />
1998- </tal:widget>
1999-
2000- <tr>
2001- <td>
2002- <label tal:replace="structure view/rcs_type_bzr">
2003- Bazaar, hosted externally
2004- </label>
2005- </td>
2006- </tr>
2007-
2008- <tr>
2009- <td>
2010- <label tal:replace="structure view/rcs_type_git">
2011- Git
2012- </label>
2013- </td>
2014- </tr>
2015-
2016- <tr>
2017- <td>
2018- <label tal:replace="structure view/rcs_type_svn">
2019- SVN
2020- </label>
2021- </td>
2022- </tr>
2023-
2024- <tr>
2025- <td>
2026- <label tal:replace="structure view/rcs_type_cvs">
2027- CVS
2028- </label>
2029- <table class="subordinate">
2030- <tal:widget define="widget nocall:view/widgets/cvs_module">
2031- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2032- </tal:widget>
2033- </table>
2034- </td>
2035- </tr>
2036-
2037- </table>
2038- </td>
2039- </tr>
2040-
2041- <tal:widget define="widget nocall:view/widgets/branch_name">
2042- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2043- </tal:widget>
2044- <tal:widget define="widget nocall:view/widgets/branch_owner">
2045- <metal:block use-macro="context/@@launchpad_form/widget_row" />
2046- </tal:widget>
2047-
2048- </table>
2049- <input tal:replace="structure view/rcs_type_emptymarker" />
2050-
2051- </metal:formbody>
2052- </div>
2053-
2054- <script type="text/javascript">
2055- LPJS.use('lp.code.productseries_setbranch', function(Y) {
2056- Y.on('domready', Y.lp.code.productseries_setbranch.setup);
2057- });
2058- </script>
2059-
2060-</div>
2061-</body>
2062-</html>
2063
2064=== modified file 'lib/lp/registry/tests/test_product.py'
2065--- lib/lp/registry/tests/test_product.py 2015-05-13 05:03:33 +0000
2066+++ lib/lp/registry/tests/test_product.py 2015-06-21 22:07:50 +0000
2067@@ -65,6 +65,7 @@
2068 SharingPermission,
2069 SpecificationSharingPolicy,
2070 TeamMembershipPolicy,
2071+ VCSType,
2072 )
2073 from lp.registry.errors import (
2074 CannotChangeInformationType,
2075@@ -309,6 +310,13 @@
2076 [u'trunk', u'active-series'],
2077 [series.name for series in active_series])
2078
2079+ def test_inferred_vcs(self):
2080+ """VCS is inferred correctly from existing branch or repo."""
2081+ git_repo = self.factory.makeGitRepository()
2082+ bzr_branch = self.factory.makeBranch()
2083+ self.assertEqual(VCSType.GIT, git_repo.target.inferred_vcs)
2084+ self.assertEqual(VCSType.BZR, bzr_branch.product.inferred_vcs)
2085+
2086 def test_owner_cannot_be_open_team(self):
2087 """Product owners cannot be open teams."""
2088 for policy in INCLUSIVE_TEAM_POLICY: