← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Review: Needs Fixing code



Diff comments:

> === modified file 'LICENSE'
> --- LICENSE	2015-03-24 13:36:23 +0000
> +++ LICENSE	2015-06-24 08:54:06 +0000
> @@ -13,6 +13,8 @@
>  The Launchpad name and logo are trademarks of Canonical, and may not
>  be used without the prior written permission of Canonical.
>  
> +Git SCM logos are licensed Creative Commons Attribution 3.0 Unported.
> +
>  Third-party copyright in this distribution is noted where applicable.
>  
>  All rights not expressly granted are reserved.
> @@ -683,3 +685,277 @@
>  <http://www.gnu.org/licenses/>.
>  
>  =========================================================================
> +
> +
> +Creative Commons Attribution 3.0 Unported License
> +
> +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
> +CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
> +PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
> +WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
> +PROHIBITED.
> +
> +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
> +AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
> +LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
> +THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH
> +TERMS AND CONDITIONS.
> +
> +1. Definitions
> +
> +"Adaptation" means a work based upon the Work, or upon the Work and
> +other pre-existing works, such as a translation, adaptation,
> +derivative work, arrangement of music or other alterations of a
> +literary or artistic work, or phonogram or performance and includes
> +cinematographic adaptations or any other form in which the Work may be
> +recast, transformed, or adapted including in any form recognizably
> +derived from the original, except that a work that constitutes a
> +Collection will not be considered an Adaptation for the purpose of
> +this License. For the avoidance of doubt, where the Work is a musical
> +work, performance or phonogram, the synchronization of the Work in
> +timed-relation with a moving image ("synching") will be considered an
> +Adaptation for the purpose of this License. "Collection" means a
> +collection of literary or artistic works, such as encyclopedias and
> +anthologies, or performances, phonograms or broadcasts, or other works
> +or subject matter other than works listed in Section 1(f) below,
> +which, by reason of the selection and arrangement of their contents,
> +constitute intellectual creations, in which the Work is included in
> +its entirety in unmodified form along with one or more other
> +contributions, each constituting separate and independent works in
> +themselves, which together are assembled into a collective whole. A
> +work that constitutes a Collection will not be considered an
> +Adaptation (as defined above) for the purposes of this License.
> +"Distribute" means to make available to the public the original and
> +copies of the Work or Adaptation, as appropriate, through sale or
> +other transfer of ownership. "Licensor" means the individual,
> +individuals, entity or entities that offer(s) the Work under the terms
> +of this License. "Original Author" means, in the case of a literary or
> +artistic work, the individual, individuals, entity or entities who
> +created the Work or if no individual or entity can be identified, the
> +publisher; and in addition (i) in the case of a performance the
> +actors, singers, musicians, dancers, and other persons who act, sing,
> +deliver, declaim, play in, interpret or otherwise perform literary or
> +artistic works or expressions of folklore; (ii) in the case of a
> +phonogram the producer being the person or legal entity who first
> +fixes the sounds of a performance or other sounds; and, (iii) in the
> +case of broadcasts, the organization that transmits the broadcast.
> +"Work" means the literary and/or artistic work offered under the terms
> +of this License including without limitation any production in the
> +literary, scientific and artistic domain, whatever may be the mode or
> +form of its expression including digital form, such as a book,
> +pamphlet and other writing; a lecture, address, sermon or other work
> +of the same nature; a dramatic or dramatico-musical work; a
> +choreographic work or entertainment in dumb show; a musical
> +composition with or without words; a cinematographic work to which are
> +assimilated works expressed by a process analogous to cinematography;
> +a work of drawing, painting, architecture, sculpture, engraving or
> +lithography; a photographic work to which are assimilated works
> +expressed by a process analogous to photography; a work of applied
> +art; an illustration, map, plan, sketch or three-dimensional work
> +relative to geography, topography, architecture or science; a
> +performance; a broadcast; a phonogram; a compilation of data to the
> +extent it is protected as a copyrightable work; or a work performed by
> +a variety or circus performer to the extent it is not otherwise
> +considered a literary or artistic work. "You" means an individual or
> +entity exercising rights under this License who has not previously
> +violated the terms of this License with respect to the Work, or who
> +has received express permission from the Licensor to exercise rights
> +under this License despite a previous violation. "Publicly Perform"
> +means to perform public recitations of the Work and to communicate to
> +the public those public recitations, by any means or process,
> +including by wire or wireless means or public digital performances; to
> +make available to the public Works in such a way that members of the
> +public may access these Works from a place and at a place individually
> +chosen by them; to perform the Work to the public by any means or
> +process and the communication to the public of the performances of the
> +Work, including by public digital performance; to broadcast and
> +rebroadcast the Work by any means including signs, sounds or images.
> +"Reproduce" means to make copies of the Work by any means including
> +without limitation by sound or visual recordings and the right of
> +fixation and reproducing fixations of the Work, including storage of a
> +protected performance or phonogram in digital form or other electronic
> +medium. 2. Fair Dealing Rights. Nothing in this License is intended to
> +reduce, limit, or restrict any uses free from copyright or rights
> +arising from limitations or exceptions that are provided for in
> +connection with the copyright protection under copyright law or other
> +applicable laws.
> +
> +3. License Grant. Subject to the terms and conditions of this License,
> +Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
> +perpetual (for the duration of the applicable copyright) license to
> +exercise the rights in the Work as stated below:
> +
> +to Reproduce the Work, to incorporate the Work into one or more
> +Collections, and to Reproduce the Work as incorporated in the
> +Collections; to create and Reproduce Adaptations provided that any
> +such Adaptation, including any translation in any medium, takes
> +reasonable steps to clearly label, demarcate or otherwise identify
> +that changes were made to the original Work. For example, a
> +translation could be marked "The original work was translated from
> +English to Spanish," or a modification could indicate "The original
> +work has been modified."; to Distribute and Publicly Perform the Work
> +including as incorporated in Collections; and, to Distribute and
> +Publicly Perform Adaptations. For the avoidance of doubt:
> +
> +Non-waivable Compulsory License Schemes. In those jurisdictions in
> +which the right to collect royalties through any statutory or
> +compulsory licensing scheme cannot be waived, the Licensor reserves
> +the exclusive right to collect such royalties for any exercise by You
> +of the rights granted under this License; Waivable Compulsory License
> +Schemes. In those jurisdictions in which the right to collect
> +royalties through any statutory or compulsory licensing scheme can be
> +waived, the Licensor waives the exclusive right to collect such
> +royalties for any exercise by You of the rights granted under this
> +License; and, Voluntary License Schemes. The Licensor waives the right
> +to collect royalties, whether individually or, in the event that the
> +Licensor is a member of a collecting society that administers
> +voluntary licensing schemes, via that society, from any exercise by
> +You of the rights granted under this License. The above rights may be
> +exercised in all media and formats whether now known or hereafter
> +devised. The above rights include the right to make such modifications
> +as are technically necessary to exercise the rights in other media and
> +formats. Subject to Section 8(f), all rights not expressly granted by
> +Licensor are hereby reserved.
> +
> +4. Restrictions. The license granted in Section 3 above is expressly
> +made subject to and limited by the following restrictions:
> +
> +You may Distribute or Publicly Perform the Work only under the terms
> +of this License. You must include a copy of, or the Uniform Resource
> +Identifier (URI) for, this License with every copy of the Work You
> +Distribute or Publicly Perform. You may not offer or impose any terms
> +on the Work that restrict the terms of this License or the ability of
> +the recipient of the Work to exercise the rights granted to that
> +recipient under the terms of the License. You may not sublicense the
> +Work. You must keep intact all notices that refer to this License and
> +to the disclaimer of warranties with every copy of the Work You
> +Distribute or Publicly Perform. When You Distribute or Publicly
> +Perform the Work, You may not impose any effective technological
> +measures on the Work that restrict the ability of a recipient of the
> +Work from You to exercise the rights granted to that recipient under
> +the terms of the License. This Section 4(a) applies to the Work as
> +incorporated in a Collection, but this does not require the Collection
> +apart from the Work itself to be made subject to the terms of this
> +License. If You create a Collection, upon notice from any Licensor You
> +must, to the extent practicable, remove from the Collection any credit
> +as required by Section 4(b), as requested. If You create an
> +Adaptation, upon notice from any Licensor You must, to the extent
> +practicable, remove from the Adaptation any credit as required by
> +Section 4(b), as requested. If You Distribute, or Publicly Perform the
> +Work or any Adaptations or Collections, You must, unless a request has
> +been made pursuant to Section 4(a), keep intact all copyright notices
> +for the Work and provide, reasonable to the medium or means You are
> +utilizing: (i) the name of the Original Author (or pseudonym, if
> +applicable) if supplied, and/or if the Original Author and/or Licensor
> +designate another party or parties (e.g., a sponsor institute,
> +publishing entity, journal) for attribution ("Attribution Parties") in
> +Licensor's copyright notice, terms of service or by other reasonable
> +means, the name of such party or parties; (ii) the title of the Work
> +if supplied; (iii) to the extent reasonably practicable, the URI, if
> +any, that Licensor specifies to be associated with the Work, unless
> +such URI does not refer to the copyright notice or licensing
> +information for the Work; and (iv) , consistent with Section 3(b), in
> +the case of an Adaptation, a credit identifying the use of the Work in
> +the Adaptation (e.g., "French translation of the Work by Original
> +Author," or "Screenplay based on original Work by Original Author").
> +The credit required by this Section 4 (b) may be implemented in any
> +reasonable manner; provided, however, that in the case of a Adaptation
> +or Collection, at a minimum such credit will appear, if a credit for
> +all contributing authors of the Adaptation or Collection appears, then
> +as part of these credits and in a manner at least as prominent as the
> +credits for the other contributing authors. For the avoidance of
> +doubt, You may only use the credit required by this Section for the
> +purpose of attribution in the manner set out above and, by exercising
> +Your rights under this License, You may not implicitly or explicitly
> +assert or imply any connection with, sponsorship or endorsement by the
> +Original Author, Licensor and/or Attribution Parties, as appropriate,
> +of You or Your use of the Work, without the separate, express prior
> +written permission of the Original Author, Licensor and/or Attribution
> +Parties. Except as otherwise agreed in writing by the Licensor or as
> +may be otherwise permitted by applicable law, if You Reproduce,
> +Distribute or Publicly Perform the Work either by itself or as part of
> +any Adaptations or Collections, You must not distort, mutilate, modify
> +or take other derogatory action in relation to the Work which would be
> +prejudicial to the Original Author's honor or reputation. Licensor
> +agrees that in those jurisdictions (e.g. Japan), in which any exercise
> +of the right granted in Section 3(b) of this License (the right to
> +make Adaptations) would be deemed to be a distortion, mutilation,
> +modification or other derogatory action prejudicial to the Original
> +Author's honor and reputation, the Licensor will waive or not assert,
> +as appropriate, this Section, to the fullest extent permitted by the
> +applicable national law, to enable You to reasonably exercise Your
> +right under Section 3(b) of this License (right to make Adaptations)
> +but not otherwise. 5. Representations, Warranties and Disclaimer
> +
> +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
> +LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
> +WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
> +STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
> +TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
> +NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
> +OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE.
> +SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES,
> +SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
> +
> +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
> +APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
> +LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR
> +EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK,
> +EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
> +
> +7. Termination
> +
> +This License and the rights granted hereunder will terminate
> +automatically upon any breach by You of the terms of this License.
> +Individuals or entities who have received Adaptations or Collections
> +from You under this License, however, will not have their licenses
> +terminated provided such individuals or entities remain in full
> +compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
> +survive any termination of this License. Subject to the above terms
> +and conditions, the license granted here is perpetual (for the
> +duration of the applicable copyright in the Work). Notwithstanding the
> +above, Licensor reserves the right to release the Work under different
> +license terms or to stop distributing the Work at any time; provided,
> +however that any such election will not serve to withdraw this License
> +(or any other license that has been, or is required to be, granted
> +under the terms of this License), and this License will continue in
> +full force and effect unless terminated as stated above. 8.
> +Miscellaneous
> +
> +Each time You Distribute or Publicly Perform the Work or a Collection,
> +the Licensor offers to the recipient a license to the Work on the same
> +terms and conditions as the license granted to You under this License.
> +Each time You Distribute or Publicly Perform an Adaptation, Licensor
> +offers to the recipient a license to the original Work on the same
> +terms and conditions as the license granted to You under this License.
> +If any provision of this License is invalid or unenforceable under
> +applicable law, it shall not affect the validity or enforceability of
> +the remainder of the terms of this License, and without further action
> +by the parties to this agreement, such provision shall be reformed to
> +the minimum extent necessary to make such provision valid and
> +enforceable. No term or provision of this License shall be deemed
> +waived and no breach consented to unless such waiver or consent shall
> +be in writing and signed by the party to be charged with such waiver
> +or consent. This License constitutes the entire agreement between the
> +parties with respect to the Work licensed here. There are no
> +understandings, agreements or representations with respect to the Work
> +not specified here. Licensor shall not be bound by any additional
> +provisions that may appear in any communication from You. This License
> +may not be modified without the mutual written agreement of the
> +Licensor and You. The rights granted under, and the subject matter
> +referenced, in this License were drafted utilizing the terminology of
> +the Berne Convention for the Protection of Literary and Artistic Works
> +(as amended on September 28, 1979), the Rome Convention of 1961, the
> +WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms
> +Treaty of 1996 and the Universal Copyright Convention (as revised on
> +July 24, 1971). These rights and subject matter take effect in the
> +relevant jurisdiction in which the License terms are sought to be
> +enforced according to the corresponding provisions of the
> +implementation of those treaty provisions in the applicable national
> +law. If the standard suite of rights granted under applicable
> +copyright law includes additional rights not granted under this
> +License, such additional rights are deemed to be included in the
> +License; this License is not intended to restrict the license of any
> +rights under applicable law.
> +
> +=========================================================================
> \ No newline at end of file
> 
> === modified file 'lib/canonical/launchpad/icing/inline-sprites-1.css.in'
> --- lib/canonical/launchpad/icing/inline-sprites-1.css.in	2015-03-24 13:36:23 +0000
> +++ lib/canonical/launchpad/icing/inline-sprites-1.css.in	2015-06-24 08:54:06 +0000
> @@ -84,7 +84,11 @@
>  .branch {
>      background-image: url(/@@/branch.png); /* sprite-ref: icon-sprites */
>      background-repeat: no-repeat;
> -    }
> +}
> +.gitbranch {
> +    background-image: url(/@@/gitbranch.png); /* sprite-ref: icon-sprites */
> +    background-repeat: no-repeat;
> +}
>  .distribution {
>      background-image: url(/@@/distribution.png); /* sprite-ref: icon-sprites */
>      background-repeat: no-repeat;
> 
> === modified file 'lib/canonical/launchpad/icing/style.css'
> --- lib/canonical/launchpad/icing/style.css	2015-04-07 23:43:43 +0000
> +++ lib/canonical/launchpad/icing/style.css	2015-06-24 08:54:06 +0000
> @@ -64,10 +64,24 @@
>      padding-left: 1em;
>  }
>  
> +form label {
> +    font-weight: bold;
> +}
> +
>  form.primary.search {
>      margin-bottom: 2em;
>  }
>  
> +.note {
> +    color: #666;
> +    display: inline-block;
> +    padding: 0.5em 0.5em 0.5em 2em;
> +    background: url(/@@/info) 0.5em 0.5em no-repeat;
> +    background-color: rgb(249, 249, 249);
> +    border: 1px solid #ddd;
> +    border-radius: 3px
> +}

This erroneously applies to the span.note in distroseriesdifferences_details.js

> +
>  div#bugs-search-form.dynamic_bug_listing {
>      margin-bottom: 10px;
>      padding: 3px 0;
> @@ -538,17 +552,36 @@
>  
>  /* --- Code --- */
>  
> +code.command {
> +    background-color: #fff;
> +    border: 1px solid #ddd;
> +    border-radius: 3px;
> +    color: #626262;
> +    padding: 4px;
> +    font-family: "DejaVu Sans Mono", "Courier New", monospace;
> +    font-size: 1.05em;
> +}
> +code.command-block {
> +    display: block;
> +    margin-bottom: 1em;
> +    padding: 6px;
> +}
> +code.command-block:last-child {
> +    margin: 0;
> +}
>  table.code {
>      margin-bottom: 1em;
> -    }
> +}
>  table.code th {
>      text-align: left;
>      font-weight: bold;
> -    }
> +}
>  table.code th, table.code td {
>      padding-right: 3em;
> -    }
> -
> +}
> +#git-expander-content {
> +    margin-top: 1em;
> +}
>  .branch-no-dev-focus {
>    background: #FFF59C;
>    vertical-align: middle;
> 
> === added file 'lib/canonical/launchpad/images/gitbranch-large.png'
> Binary 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-24 08:54:06 +0000 differ
> === added file 'lib/canonical/launchpad/images/gitbranch.png'
> Binary files lib/canonical/launchpad/images/gitbranch.png	1970-01-01 00:00:00 +0000 and lib/canonical/launchpad/images/gitbranch.png	2015-06-24 08:54:06 +0000 differ
> === modified file 'lib/lp/code/browser/branchlisting.py'
> --- lib/lp/code/browser/branchlisting.py	2015-06-15 05:28:12 +0000
> +++ lib/lp/code/browser/branchlisting.py	2015-06-24 08:54:06 +0000
> @@ -1133,13 +1133,12 @@
>      @property
>      def configure_codehosting(self):
>          """Get the menu link for configuring code hosting."""
> -        if not check_permission(
> -            'launchpad.Edit', self.context.development_focus):
> +        if not check_permission('launchpad.Edit', self.context):
>              return None
> -        series_menu = MenuAPI(self.context.development_focus).overview
> -        set_branch = series_menu['set_branch']
> -        set_branch.text = 'Configure Code'
> -        return set_branch
> +        menu = MenuAPI(self.context).overview
> +        configure_code = menu['configure_code']
> +        configure_code.text = 'Configure Code'
> +        return configure_code

What does this override from the original? Does anything still use the original?

>  
>  
>  class ProductBranchStatisticsView(BranchCountSummaryView,
> 
> === modified file 'lib/lp/code/browser/configure.zcml'
> --- lib/lp/code/browser/configure.zcml	2015-06-18 20:18:16 +0000
> +++ lib/lp/code/browser/configure.zcml	2015-06-24 08:54:06 +0000
> @@ -37,7 +37,34 @@
>        template="../templates/code-in-branches.pt"
>        permission="zope.Public"
>        />
> -
> +  <browser:page
> +      name="+configure-code"
> +      for="lp.registry.interfaces.product.IProduct"
> +      class="lp.registry.browser.product.ProductSetBranchView"
> +      permission="launchpad.Edit"
> +      template="../templates/configure-code.pt"
> +      />
> +  <browser:page
> +      name="+setbranch"
> +      for="lp.registry.interfaces.productseries.IProductSeries"
> +      class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
> +      permission="launchpad.Edit"
> +      template="../templates/configure-code.pt"
> +      />
> +  <browser:page
> +      name="+configure-code-macros"
> +      for="lp.registry.interfaces.productseries.IProductSeries"
> +      class="lp.app.browser.launchpad.Macro"
> +      permission="zope.Public"
> +      template="../templates/configure-code-macros.pt"
> +      />
> +    <browser:page
> +      name="+configure-code-macros"
> +      for="lp.registry.interfaces.product.IProduct"
> +      class="lp.app.browser.launchpad.Macro"
> +      permission="zope.Public"
> +      template="../templates/configure-code-macros.pt"
> +      />
>    <browser:page
>        for="lp.services.webapp.interfaces.ILaunchpadApplication"
>        name="+project-cloud"
> 
> === modified file 'lib/lp/code/javascript/productseries-setbranch.js'
> --- lib/lp/code/javascript/productseries-setbranch.js	2015-05-19 03:01:06 +0000
> +++ lib/lp/code/javascript/productseries-setbranch.js	2015-06-24 08:54:06 +0000
> @@ -24,6 +24,17 @@
>          return selected;
>      };
>  
> +    module._get_selected_default_vcs = function () {
> +        var vcs = document.getElementsByName('field.default_vcs');
> +        var i;
> +        for (i = 0; i < vcs.length; i++) {
> +            if (vcs[i].checked) {
> +                return vcs[i].value;
> +            } 
> +        }
> +        return null;
> +    };
> +
>  
>      module.__rcs_types = null;
>  
> @@ -39,6 +50,32 @@
>          field.disabled = !is_enabled;
>      };
>  
> +    module.setup_expanders = function() {
> +        var git_expander = new Y.lp.app.widgets.expander.Expander(
> +            Y.one('#git-expander-icon'), Y.one('#git-expander-content')
> +        );
> +
> +        var bzr_expander = new Y.lp.app.widgets.expander.Expander(
> +            Y.one('#bzr-expander-icon'), Y.one('#bzr-expander-content')
> +        );
> +        module.git_expander = git_expander.setUp();
> +        module.bzr_expander = bzr_expander.setUp();
> +    };
> +
> +    module.onclick_default_vcs = function(e) {
> +        /* Which project vcs was selected? */
> +        var selectedDefaultVCS =
> +                module._get_selected_default_vcs();
> +
> +        if (selectedDefaultVCS === 'GIT') {
> +            module.git_expander.render(true);
> +            module.bzr_expander.render(false);
> +        } else {
> +            module.bzr_expander.render(true);
> +            module.git_expander.render(false);
> +        }
> +    };
> +
>      module.onclick_rcs_type = function(e) {
>          /* Which rcs type radio button has been selected? */
>          // CVS
> @@ -81,10 +118,14 @@
>              'click', module.onclick_rcs_type);
>          Y.all('input[name="field.branch_type"]').on(
>              'click', module.onclick_branch_type);
> +        Y.all('input[name="field.default_vcs"]').on(
> +            'click', module.onclick_default_vcs);
>  
>          // Set the initial state.
> +        module.setup_expanders();
>          module.onclick_rcs_type();
>          module.onclick_branch_type();
> +        module.onclick_default_vcs();
>      };
>  
>  }, "0.1", {"requires": ["node", "DOM"]});
> 
> === modified file 'lib/lp/code/model/gitrepository.py'
> --- lib/lp/code/model/gitrepository.py	2015-06-15 09:55:47 +0000
> +++ lib/lp/code/model/gitrepository.py	2015-06-24 08:54:06 +0000
> @@ -307,7 +307,7 @@
>              # Check for an existing target default.
>              existing = getUtility(IGitRepositorySet).getDefaultRepository(
>                  self.target)
> -            if existing is not None:
> +            if existing is not None and existing != self:
>                  raise GitDefaultConflict(existing, self.target)
>          self.target_default = value
>          if IProduct.providedBy(self.target):

This should be fixed in setOwnerDefault too.

> 
> === modified file 'lib/lp/code/model/tests/test_gitrepository.py'
> --- lib/lp/code/model/tests/test_gitrepository.py	2015-06-18 14:13:40 +0000
> +++ lib/lp/code/model/tests/test_gitrepository.py	2015-06-24 08:54:06 +0000
> @@ -1986,6 +1986,17 @@
>                  self.repository_set.setDefaultRepositoryForOwner,
>                  person, person, repository, user)
>  
> +    def test_setDefaultRepository_for_same_targetdefault_noops(self):
> +        # If a repository is already the target default,
> +        # setting the defaultRepository again should no-op.
> +        project = self.factory.makeProduct()
> +        repository = self.factory.makeGitRepository(target=project)
> +        with person_logged_in(project.owner):
> +            self.repository_set.setDefaultRepository(project, repository)
> +            result = self.repository_set.setDefaultRepository(
> +                project, repository)
> +        self.assertEqual(None, result)
> +
>  
>  class TestGitRepositorySetDefaultsMixin:
>  
> 
> === modified file 'lib/lp/code/stories/codeimport/xx-create-codeimport.txt'
> --- lib/lp/code/stories/codeimport/xx-create-codeimport.txt	2015-06-15 05:28:12 +0000
> +++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt	2015-06-24 08:54:06 +0000
> @@ -38,11 +38,12 @@
>      >>> owner_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
>      >>> owner_browser.open('http://code.launchpad.dev/firefox')
>      >>> owner_browser.getLink('Configure Code').click()
> +    >>> owner_browser.getControl('Bazaar', index=0).click()
>      >>> owner_browser.getControl(
>      ...     'Import a branch').click()
>      >>> owner_browser.getControl('Branch URL').value = (
>      ...     'git://example.com/firefox')
> -    >>> owner_browser.getControl('Git').click()
> +    >>> owner_browser.getControl('Git', index=1).click()
>      >>> owner_browser.getControl('Branch name').value = 'trunk'
>      >>> owner_browser.getControl('Update').click()
>  
> 
> === added file 'lib/lp/code/templates/configure-code-macros.pt'
> --- lib/lp/code/templates/configure-code-macros.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/configure-code-macros.pt	2015-06-24 08:54:06 +0000
> @@ -0,0 +1,39 @@
> +<tal:root
> +    xmlns:tal="http://xml.zope.org/namespaces/tal";
> +    xmlns:metal="http://xml.zope.org/namespaces/metal";
> +    xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +    omit-tag="">
> +
> +  <div metal:define-macro="push-instructions-bzr" id="push-instructions-bzr" class="scm-tip">

This indentation is inconsistent with the rest of the file.

> +      <h3>Push a new branch</h3>
> +      <p>You can push a Bazaar branch directly to Launchpad with the command:</p>
> +      <p>
> +        <code class="command command-block">
> +          bzr push lp:<tal:project replace="context/name"/>
> +        </code>
> +      </p>
> +    </div>
> +
> +    <div metal:define-macro="push-instructions-git" id="push-instructions-git" class="scm-tip">
> +      <h3>Push a new repository</h3>
> +      <p>You can add a remote for your Git branch with the

s/branch/repository/

> +      command:</p>
> +      <p>
> +      <code class="command command-block">
> +        git remote add origin <tal:project replace="modules/lp.services.config/config/codehosting/git_ssh_root"/><tal:project replace="context/name"/><br />
> +      </code></p>
> +      <p>&hellip; and push the git branch to Launchpad with:</p>

s/git/Git/

> +      <p>
> +      <code class="command command-block">
> +        git push origin master
> +      </code>
> +      </p>
> +    </div>
> +
> +    <div metal:define-macro="no-keys" condition="not:view/user/sshkeys">
> +      <p class="note">To authenticate with the Launchpad branch upload service, you need to
> +      <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
> +      register a SSH key</a>.</p>

"an SSH key"

> +    </div>
> +
> +</tal:root>
> 
> === added file 'lib/lp/code/templates/configure-code.pt'
> --- lib/lp/code/templates/configure-code.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/configure-code.pt	2015-06-24 08:54:06 +0000
> @@ -0,0 +1,167 @@
> +<html
> +    xmlns="http://www.w3.org/1999/xhtml";
> +    xmlns:tal="http://xml.zope.org/namespaces/tal";
> +    xmlns:metal="http://xml.zope.org/namespaces/metal";
> +    xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +    metal:use-macro="view/macro:page/main_only"
> +    i18n:domain="launchpad">
> +
> +  <body>
> +
> +    <metal:block fill-slot="head_epilogue">
> +      <style type="text/css">
> +        .subordinate {
> +        margin: 0.5em 0 0.5em 4em;
> +        }
> +      </style>
> +    </metal:block>
> +
> +    <div metal:fill-slot="main">
> +
> +      <div metal:use-macro="context/@@launchpad_form/form">
> +
> +        <metal:formbody fill-slot="widgets">
> +          <tal:block condition="not: view/is_series">
> +            <h3>Version control system</h3>
> +            <div id="default_vcs">
> +              <ul>
> +                <li>
> +                  <label tal:replace="structure view/default_vcs_bzr">
> +                    Bazaar
> +                  </label>
> +                </li>
> +                <li>
> +                  <label tal:replace="structure view/default_vcs_git">
> +                    Git
> +                    </label> (beta)

Consider replacing the (beta) with something like <div style="display: inline" class="beta"><img alt="[BETA]" src="/@@/beta"></div>

> +                </li>
> +              </ul>
> +              <p>
> +                <span class="note">Your project may have both
> +                Git repositories and Bazaar branches.</span><br/>

I'm not sure this is important enough for .note. In the Git case you end up with three (i)s on screen at once, which looks quite odd.

> +              </p>
> +            </div>
> +          </tal:block>
> +
> +          <div id="show-hide-bzr">
> +            <a href="#" id="bzr-expander-icon" class="expander-icon js-action">
> +              <span class="sprite branch">Bazaar settings</span>
> +            </a>
> +            <div id="bzr-expander-content">
> +              <div class="push-instructions">
> +                <div metal:use-macro="context/@@+configure-code-macros/push-instructions-bzr"></div>
> +                <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
> +              </div>
> +
> +              <table id="form_bzr" class="form">
> +                <tr>
> +                  <td>
> +                    <h3>Link or Import an existing branch</h3>

s/Import/import/

> +                    <label tal:replace="structure view/branch_type_link">
> +                      Link to a Bazaar branch already in Launchpad

s/in/on/

> +                    </label>
> +                    <table class="subordinate">
> +                      <tal:widget define="widget nocall:view/widgets/branch_location">
> +                        <metal:block use-macro="context/@@launchpad_form/widget_row" />
> +                      </tal:widget>
> +                    </table>
> +                  </td>
> +                </tr>
> +
> +                <tr id="branch_mirror">
> +                  <td>
> +                    <label tal:replace="structure view/branch_type_import">
> +                      Import a branch hosted somewhere else
> +                    </label>
> +                    <table class="subordinate">
> +                      <tal:widget define="widget nocall:view/widgets/branch_name">
> +                        <metal:block use-macro="context/@@launchpad_form/widget_row" />
> +                      </tal:widget>
> +                      <tal:widget define="widget nocall:view/widgets/branch_owner">
> +                        <metal:block use-macro="context/@@launchpad_form/widget_row" />
> +                      </tal:widget>
> +
> +                      <tal:widget define="widget nocall:view/widgets/repo_url">
> +                        <metal:block use-macro="context/@@launchpad_form/widget_row" />
> +                      </tal:widget>
> +
> +                      <tr>
> +                        <td>
> +                          <label tal:replace="structure view/rcs_type_bzr">
> +                            Bazaar, hosted externally
> +                          </label>
> +                        </td>
> +                      </tr>
> +
> +                      <tr>
> +                        <td>
> +                          <label tal:replace="structure view/rcs_type_git">
> +                            Git
> +                          </label>
> +                        </td>
> +                      </tr>
> +
> +                      <tr>
> +                        <td>
> +                          <label tal:replace="structure view/rcs_type_svn">
> +                            SVN
> +                          </label>
> +                        </td>
> +                      </tr>
> +
> +                      <tr>
> +                        <td>
> +                          <label tal:replace="structure view/rcs_type_cvs">
> +                            CVS
> +                          </label>
> +                          <table class="subordinate">
> +                            <tal:widget define="widget nocall:view/widgets/cvs_module">
> +                              <metal:block use-macro="context/@@launchpad_form/widget_row" />
> +                            </tal:widget>
> +                          </table>
> +                        </td>
> +                      </tr>
> +                    </table>
> +                  </td>
> +                </tr>
> +              </table>
> +            </div>
> +            <input tal:replace="structure view/rcs_type_emptymarker" />
> +          </div>
> +
> +          <tal:block condition="not: view/is_series">
> +          <div id="show-hide-git">
> +            <a href="#" id="git-expander-icon" class="expander-icon js-action">
> +              <span class="sprite gitbranch">Git settings</span>
> +            </a>
> +            <div id="git-expander-content">
> +              <div id="form_git" class="form">
> +                <span class="note">Git support is currently in beta.</span>

With the obvious burgundy div.beta on the radio buttons this may be unnecessary clutter. The many (i)s again look a bit weird.

> +                <div class="push-instructions">
> +                  <div metal:use-macro="context/@@+configure-code-macros/push-instructions-git"></div>
> +                  <div metal:use-macro="context/@@+configure-code-macros/no-keys"></div>
> +                </div>
> +
> +                <h3>Link an existing repository</h3>
> +                <p>Link to an existing Git repository already in Launchpad</p>

s/in/on/

> +                <tal:widget define="widget nocall:view/widgets/git_repository_location">
> +                  <metal:block use-macro="context/@@launchpad_form/widget_row" />
> +                </tal:widget>
> +              </div>
> +            </div>
> +          </div>
> +          </tal:block>
> +
> +
> +        </metal:formbody>
> +      </div>
> +
> +      <script type="text/javascript">
> +        LPJS.use('lp.code.productseries_setbranch', function(Y) {
> +        Y.on('domready', Y.lp.code.productseries_setbranch.setup);
> +        });
> +      </script>
> +
> +    </div>
> +  </body>
> +</html>
> 
> === modified file 'lib/lp/code/templates/product-branch-summary.pt'
> --- lib/lp/code/templates/product-branch-summary.pt	2015-06-04 06:54:09 +0000
> +++ lib/lp/code/templates/product-branch-summary.pt	2015-06-24 08:54:06 +0000
> @@ -56,8 +56,7 @@
>      </p>
>    </div>
>  
> -  <tal:no-branches
> -      condition="not: view/branch_count">
> +  <tal:no-branches condition="not: view/branch_count">
>      There are no branches for <tal:project-name replace="context/displayname"/>
>      in Launchpad.
>      <tal:can-configure condition="view/can_configure_branches">
> 
> === modified file 'lib/lp/code/vocabularies/configure.zcml'
> --- lib/lp/code/vocabularies/configure.zcml	2015-04-13 19:02:15 +0000
> +++ lib/lp/code/vocabularies/configure.zcml	2015-06-24 08:54:06 +0000
> @@ -66,4 +66,15 @@
>      <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
>    </class>
>  
> +    <securedutility

Anomalous indentation.

> +    name="GitRepositoryRestrictedOnProduct"
> +    component=".gitrepository.GitRepositoryRestrictedOnProductVocabulary"
> +    provides="zope.schema.interfaces.IVocabularyFactory">
> +    <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
> +  </securedutility>
> +
> +  <class class=".gitrepository.GitRepositoryRestrictedOnProductVocabulary">
> +    <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
> +  </class>
> +
>  </configure>
> 
> === modified file 'lib/lp/code/vocabularies/gitrepository.py'
> --- lib/lp/code/vocabularies/gitrepository.py	2015-04-28 15:22:46 +0000
> +++ lib/lp/code/vocabularies/gitrepository.py	2015-06-24 08:54:06 +0000
> @@ -6,6 +6,7 @@
>  __metaclass__ = type
>  
>  __all__ = [
> +    'GitRepositoryRestrictedOnProductVocabulary',
>      'GitRepositoryVocabulary',
>      ]
>  
> @@ -15,6 +16,7 @@
>  
>  from lp.code.interfaces.gitcollection import IAllGitRepositories
>  from lp.code.model.gitrepository import GitRepository
> +from lp.registry.interfaces.product import IProduct
>  from lp.services.webapp.interfaces import ILaunchBag
>  from lp.services.webapp.vocabulary import (
>      CountableIterator,
> @@ -62,3 +64,20 @@
>  
>      def _getCollection(self):
>          return getUtility(IAllGitRepositories)
> +
> +class GitRepositoryRestrictedOnProductVocabulary(GitRepositoryVocabulary):
> +    """A vocabulary for searching git repositories restricted on product."""
> +
> +    def __init__(self, context=None):
> +        super(GitRepositoryRestrictedOnProductVocabulary, self).__init__(
> +            context)
> +        if IProduct.providedBy(self.context):
> +            self.product = self.context
> +        else:
> +            # An unexpected type.
> +            raise AssertionError('Unexpected context type')

context is an optional keyword argument, but if omitted it will blow up. I'd drop the =None.

> +
> +    def _getCollection(self):
> +        return getUtility(IAllGitRepositories).inProject(
> +            self.product).isExclusive()
> +
> 
> === modified file 'lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py'
> --- lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py	2015-05-14 13:57:51 +0000
> +++ lib/lp/code/vocabularies/tests/test_gitrepository_vocabularies.py	2015-06-24 08:54:06 +0000
> @@ -5,9 +5,15 @@
>  
>  __metaclass__ = type
>  
> -from lp.code.vocabularies.gitrepository import GitRepositoryVocabulary
> +from zope.component import getUtility
> +
> +from lp.code.vocabularies.gitrepository import (
> +    GitRepositoryRestrictedOnProductVocabulary,
> +    GitRepositoryVocabulary,
> +    )
>  from lp.testing import TestCaseWithFactory
>  from lp.testing.layers import DatabaseFunctionalLayer
> +from lp.registry.interfaces.product import IProductSet
>  
>  
>  class TestGitRepositoryVocabulary(TestCaseWithFactory):
> @@ -50,3 +56,42 @@
>          # If there are more than one search result, a LookupError is still
>          # raised.
>          self.assertRaises(LookupError, self.vocab.getTermByToken, "fizzbuzz")
> +
> +
> +class TestRestrictedGitRepositoryVocabularyOnProduct(TestCaseWithFactory):
> +    """Test the GitRepositoryRestrictedOnProductVocabulary behaves as expected.
> +
> +    When a GitRepositoryRestrictedOnProductVocabulary is used with a project,
> +    the project of the git repository in the vocabulary match the product give
> +    as the context.
> +    """
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def setUp(self):
> +        super(TestRestrictedGitRepositoryVocabularyOnProduct, self).setUp()
> +        self._createRepositories()
> +        self.vocab = GitRepositoryRestrictedOnProductVocabulary(
> +            context=self._getVocabRestriction())
> +
> +    def _getVocabRestriction(self):
> +        """Restrict using the widget product."""
> +        return getUtility(IProductSet).getByName('widget')
> +
> +    def _createRepositories(self):
> +        test_product = self.factory.makeProduct(name='widget')
> +        person = self.factory.makePerson(name=u'scotty')
> +        self.factory.makeProductGitRepository(
> +            owner=person, project=test_product, name=u'mountain')
> +        self.product = test_product

I'd create a repo for another Product so tests will fail if the inProject(...) breaks.

> +
> +    def test_singleQueryResult(self):
> +        # If there is a single search result that matches, use that
> +        # as the result.
> +        term = self.vocab.getTermByToken('mountain')
> +        self.assertEqual(u'~scotty/widget/+git/mountain', term.value.unique_name)
> +
> +    def test_multipleQueryResult(self):
> +        # If there are more than one search result, a LookupError is still
> +        # raised.
> +        self.assertRaises(LookupError, self.vocab.getTermByToken, 'scotty')
> 
> === modified file 'lib/lp/registry/browser/configure.zcml'
> --- lib/lp/registry/browser/configure.zcml	2015-06-10 10:19:25 +0000
> +++ lib/lp/registry/browser/configure.zcml	2015-06-24 08:54:06 +0000
> @@ -2002,13 +2002,6 @@
>          template="../templates/milestone-add.pt"
>          />
>      <browser:page
> -        name="+setbranch"
> -        for="lp.registry.interfaces.productseries.IProductSeries"
> -        class="lp.registry.browser.productseries.ProductSeriesSetBranchView"
> -        permission="launchpad.Edit"
> -        template="../templates/productseries-setbranch.pt"
> -        />
> -    <browser:page
>          name="+review"
>          for="lp.registry.interfaces.productseries.IProductSeries"
>          class="lp.registry.browser.productseries.ProductSeriesReviewView"
> 
> === modified file 'lib/lp/registry/browser/product.py'
> --- lib/lp/registry/browser/product.py	2015-06-15 05:28:12 +0000
> +++ lib/lp/registry/browser/product.py	2015-06-24 08:54:06 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Browser views for products."""
> @@ -30,6 +30,7 @@
>      'ProductRdfView',
>      'ProductReviewLicenseView',
>      'ProductSeriesSetView',
> +    'ProductSetBranchView',
>      'ProductSetBreadcrumb',
>      'ProductSetNavigation',
>      'ProductSetReviewLicensesView',
> @@ -44,8 +45,12 @@
>  
>  from operator import attrgetter
>  
> +from bzrlib.revision import NULL_REVISION
>  from lazr.delegates import delegates
> -from lazr.restful.interface import copy_field
> +from lazr.restful.interface import (
> +    copy_field,
> +    use_template,
> +    )
>  from lazr.restful.interfaces import IJSONRequestCache
>  from z3c.ptcompat import ViewPageTemplateFile
>  from zope.component import getUtility
> @@ -66,6 +71,7 @@
>  from zope.schema import (
>      Bool,
>      Choice,
> +    TextLine,
>      )
>  from zope.schema.vocabulary import (
>      SimpleTerm,
> @@ -80,6 +86,7 @@
>      custom_widget,
>      LaunchpadEditFormView,
>      LaunchpadFormView,
> +    render_radio_widget_part,
>      ReturnToReferrerMixin,
>      safe_action,
>      )
> @@ -103,7 +110,10 @@
>      PUBLIC_PROPRIETARY_INFORMATION_TYPES,
>      ServiceUsage,
>      )
> -from lp.app.errors import NotFoundError
> +from lp.app.errors import (
> +    NotFoundError,
> +    UnexpectedFormData,
> +    )
>  from lp.app.interfaces.launchpad import ILaunchpadCelebrities
>  from lp.app.utilities import json_dump_information_types
>  from lp.app.vocabularies import InformationTypeVocabulary
> @@ -131,9 +141,28 @@
>      StructuralSubscriptionTargetTraversalMixin,
>      )
>  from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
> +from lp.code.browser.branch import BranchNameValidationMixin
>  from lp.code.browser.branchref import BranchRef
> +from lp.code.browser.codeimport import validate_import_url
>  from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
> +from lp.code.enums import (
> +    BranchType,
> +    RevisionControlSystems,
> +    )
> +from lp.code.errors import (
> +    BranchCreationForbidden,
> +    BranchExists,
> +    )
> +from lp.code.interfaces.branch import IBranch
> +from lp.code.interfaces.branchjob import IRosettaUploadJobSource
> +from lp.code.interfaces.branchtarget import IBranchTarget
> +from lp.code.interfaces.codeimport import (
> +    ICodeImport,
> +    ICodeImportSet,
> +    )
> +from lp.code.interfaces.gitrepository import IGitRepositorySet
>  from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
> +
>  from lp.registry.browser import (
>      add_subscribe_link,
>      BaseRdfView,
> @@ -150,6 +179,7 @@
>      PillarNavigationMixin,
>      PillarViewMixin,
>      )
> +from lp.registry.enums import VCSType
>  from lp.registry.interfaces.pillar import IPillarNameSet
>  from lp.registry.interfaces.product import (
>      IProduct,
> @@ -171,6 +201,7 @@
>  from lp.services.fields import (
>      PillarAliases,
>      PublicPersonChoice,
> +    URIField,
>      )
>  from lp.services.librarian.interfaces import ILibraryFileAliasSet
>  from lp.services.propertycache import cachedproperty
> @@ -347,7 +378,6 @@
>          'configured' -- a boolean representing the configuration status.
>          """
>          overview_menu = MenuAPI(self.context).overview
> -        series_menu = MenuAPI(self.context.development_focus).overview
>          configuration_names = [
>              'configure_bugtracker',
>              'configure_translations',
> @@ -363,11 +393,11 @@
>                                      configured=config_statuses[key]))
>  
>          # Add the branch configuration in separately.
> -        set_branch = series_menu['set_branch']
> -        set_branch.text = 'Code'
> -        set_branch.summary = "Specify the location of this project's code."
> +        configure_code = overview_menu['configure_code']
> +        configure_code.text = 'Code'
> +        configure_code.summary = "Specify the location of this project's code."
>          config_list.insert(0,
> -            dict(link=set_branch,
> +            dict(link=configure_code,
>                   configured=config_statuses['configure_codehosting']))
>          return config_list
>  
> @@ -506,6 +536,7 @@
>          'packages',
>          'series',
>          'series_add',
> +        'configure_code',
>          'milestones',
>          'downloads',
>          'announce',
> @@ -559,6 +590,14 @@
>              'RDF</abbr> metadata')
>          return Link('+rdf', text, icon='download')
>  
> +    @enabled_with_permission('launchpad.Edit')
> +    def configure_code(self):
> +        """Return a link to configure code for this project."""
> +        text = 'Configure code'
> +        icon = 'edit'
> +        summary = 'Configure code for this project'
> +        return Link('+configure-code', text, summary, icon=icon)
> +
>      def downloads(self):
>          text = 'Downloads'
>          return Link('+download', text, icon='info')
> @@ -1594,6 +1633,379 @@
>          return BatchNavigator(decorated_result, self.request)
>  
>  
> +LINK_LP_BZR = 'link-lp-bzr'
> +IMPORT_EXTERNAL = 'import-external'
> +
> +
> +BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
> +    SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
> +               _("Link to a Bazaar branch already on Launchpad")),
> +    SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
> +               _("Import a branch hosted somewhere else")),
> +    ))
> +
> +
> +class SetBranchForm(Interface):
> +    """The fields presented on the form for setting a branch."""
> +
> +    use_template(ICodeImport, ['cvs_module'])
> +
> +    rcs_type = Choice(title=_("Type of RCS"),
> +        required=False, vocabulary=RevisionControlSystems,
> +        description=_(
> +            "The version control system to import from. "))
> +
> +    repo_url = URIField(
> +        title=_("Branch URL"), required=True,
> +        description=_("The URL of the branch."),
> +        allowed_schemes=["http", "https"],
> +        allow_userinfo=False, allow_port=True, allow_query=False,
> +        allow_fragment=False, trailing_slash=False)
> +
> +    branch_location = copy_field(
> +        IProductSeries['branch'], __name__='branch_location',
> +        title=_('Branch'),
> +        description=_(
> +            "The Bazaar branch for this series in Launchpad, "
> +            "if one exists."))
> +
> +    branch_type = Choice(
> +        title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
> +        description=_("The type of import"), required=True)
> +
> +    branch_name = copy_field(
> +        IBranch['name'], __name__='branch_name', title=_('Branch name'),
> +        description=_(''), required=True)
> +
> +    branch_owner = copy_field(
> +        IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
> +        description=_(''), required=True)
> +
> +
> +def create_git_fields():
> +    return form.Fields(
> +        Choice(__name__='default_vcs',
> +               title=_("Project VCS"),
> +               required=True, vocabulary=VCSType,
> +               description=_("The version control system for "
> +                             "this project.")),
> +        Choice(__name__='git_repository_location',
> +               title=_('Git Repository'),

This is the only title-case field on the form. s/Repository/repository/

> +               required=False,
> +               vocabulary='GitRepositoryRestrictedOnProduct',
> +               description=_(
> +                   "The Git repository for this project in Launchpad, "
> +                   "if one exists, in the form: "
> +                   "~user/project-name/+git/repo-name"))
> +    )
> +
> +
> +class ProductSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
> +                           ProductView,
> +                           BranchNameValidationMixin):
> +    """The view to set a branch default for the Product."""
> +
> +    label = 'Configure code'
> +    page_title = label
> +    schema = SetBranchForm
> +    # Set for_input to True to ensure fields marked read-only will be editable
> +    # upon creation.
> +    for_input = True
> +
> +    custom_widget('rcs_type', LaunchpadRadioWidget)
> +    custom_widget('branch_type', LaunchpadRadioWidget)
> +    custom_widget('default_vcs', LaunchpadRadioWidget)
> +
> +    errors_in_action = False
> +
> +    @property
> +    def is_series(self):
> +        return False

This need not be a property.

> +
> +    @property
> +    def series(self):
> +        return self.context.development_focus
> +
> +    @property
> +    def initial_values(self):
> +        return dict(
> +            rcs_type=RevisionControlSystems.BZR,
> +            default_vcs=(self.context.pillar.inferred_vcs or VCSType.BZR),
> +            branch_type=LINK_LP_BZR,
> +            branch_location=self.series.branch)
> +
> +    @property
> +    def next_url(self):
> +        """Return the next_url.
> +
> +        Use the value from `ReturnToReferrerMixin` or None if there
> +        are errors.
> +        """
> +        if self.errors_in_action:
> +            return None
> +        return super(ProductSetBranchView, self).next_url
> +
> +    def setUpFields(self):
> +        """See `LaunchpadFormView`."""
> +        super(ProductSetBranchView, self).setUpFields()
> +        if not self.is_series:
> +            self.form_fields = (self.form_fields + create_git_fields())
> +
> +    def setUpWidgets(self):
> +        """See `LaunchpadFormView`."""
> +        super(ProductSetBranchView, self).setUpWidgets()
> +        widget = self.widgets['rcs_type']
> +        vocab = widget.vocabulary
> +        current_value = widget._getFormValue()
> +        self.rcs_type_cvs = render_radio_widget_part(
> +            widget, vocab.CVS, current_value, 'CVS')
> +        self.rcs_type_svn = render_radio_widget_part(
> +            widget, vocab.BZR_SVN, current_value, 'SVN')
> +        self.rcs_type_git = render_radio_widget_part(
> +            widget, vocab.GIT, current_value)
> +        self.rcs_type_bzr = render_radio_widget_part(
> +            widget, vocab.BZR, current_value)
> +        self.rcs_type_emptymarker = widget._emptyMarker()
> +
> +        widget = self.widgets['branch_type']
> +        current_value = widget._getFormValue()
> +        vocab = widget.vocabulary
> +
> +        (self.branch_type_link,
> +         self.branch_type_import) = [
> +            render_radio_widget_part(widget, value, current_value)
> +            for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
> +
> +        if not self.is_series:
> +            widget = self.widgets['default_vcs']
> +            vocab = widget.vocabulary
> +            current_value = widget._getFormValue()
> +            self.default_vcs_git = render_radio_widget_part(
> +                widget, vocab.GIT, current_value, 'Git')
> +            self.default_vcs_bzr = render_radio_widget_part(
> +                widget, vocab.BZR, current_value, 'Bazaar')
> +
> +    def _validateLinkLpBzr(self, data):
> +        """Validate data for link-lp-bzr case."""
> +        if 'branch_location' not in data:
> +            self.setFieldError(
> +                'branch_location', 'The branch location must be set.')
> +
> +    def _validateLinkLpGit(self, data):
> +        """Validate data for link-lp-git case."""
> +        if data.get('git_repository_location'):
> +            repo = data.get('git_repository_location')
> +            if not repo:
> +                self.setFieldError(
> +                    'git_repository_location',
> +                    'The repository does not exist.')
> +            if not (self.context == repo.target):
> +                self.setFieldError(
> +                    'git_repository_location',
> +                    'The repository is in a different project.')

Does the target check need to exist now that the vocab is in place?

> +
> +    def _validateImportExternal(self, data):
> +        """Validate data for import external case."""
> +        rcs_type = data.get('rcs_type')
> +        repo_url = data.get('repo_url')
> +
> +        # Private teams are forbidden from owning code imports.
> +        branch_owner = data.get('branch_owner')
> +        if branch_owner is not None and branch_owner.private:
> +            self.setFieldError(
> +                'branch_owner', 'Private teams are forbidden from owning '
> +                'external imports.')
> +
> +        if repo_url is None:
> +            self.setFieldError(
> +                'repo_url', 'You must set the external repository URL.')
> +        else:
> +            reason = validate_import_url(repo_url)
> +            if reason:
> +                self.setFieldError('repo_url', reason)
> +
> +        # RCS type is mandatory.
> +        # This condition should never happen since an initial value is set.
> +        if rcs_type is None:
> +            # The error shows but does not identify the widget.
> +            self.setFieldError(
> +                'rcs_type',
> +                'You must specify the type of RCS for the remote host.')
> +        elif rcs_type == RevisionControlSystems.CVS:
> +            if 'cvs_module' not in data:
> +                self.setFieldError('cvs_module', 'The CVS module must be set.')
> +        self._validateBranch(data)
> +
> +    def _validateBranch(self, data):
> +        """Validate that branch name and owner are set."""
> +        if 'branch_name' not in data:
> +            self.setFieldError('branch_name', 'The branch name must be set.')
> +        if 'branch_owner' not in data:
> +            self.setFieldError('branch_owner', 'The branch owner must be set.')
> +
> +    def _setRequired(self, names, value):
> +        """Mark the widget field as optional."""
> +        for name in names:
> +            widget = self.widgets[name]
> +            # The 'required' property on the widget context is set to False.
> +            # The widget also has a 'required' property but it isn't used
> +            # during validation.
> +            widget.context.required = value
> +
> +    def _validSchemes(self, rcs_type):
> +        """Return the valid schemes for the repository URL."""
> +        schemes = set(['http', 'https'])
> +        # Extend the allowed schemes for the repository URL based on
> +        # rcs_type.
> +        extra_schemes = {
> +            RevisionControlSystems.BZR_SVN: ['svn'],
> +            RevisionControlSystems.GIT: ['git'],
> +            RevisionControlSystems.BZR: ['bzr'],
> +            }
> +        schemes.update(extra_schemes.get(rcs_type, []))
> +        return schemes
> +
> +    def validate_widgets(self, data, names=None):
> +        """See `LaunchpadFormView`."""
> +        names = ['branch_type', 'rcs_type', 'default_vcs']
> +        super(ProductSetBranchView, self).validate_widgets(data, names)
> +        branch_type = data.get('branch_type')
> +
> +        if branch_type == LINK_LP_BZR:
> +            # Mark other widgets as non-required.
> +            self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
> +                               'branch_name', 'branch_owner'], False)
> +        elif branch_type == IMPORT_EXTERNAL:
> +            rcs_type = data.get('rcs_type')
> +
> +            # Set the valid schemes based on rcs_type.
> +            self.widgets['repo_url'].field.allowed_schemes = (
> +                self._validSchemes(rcs_type))
> +            # The branch location is not required for validation.
> +            self._setRequired(['branch_location'], False)
> +            # The cvs_module is required if it is a CVS import.
> +            if rcs_type == RevisionControlSystems.CVS:
> +                self._setRequired(['cvs_module'], True)
> +        else:
> +            raise AssertionError("Unknown branch type %s" % branch_type)
> +        # Perform full validation now.
> +        super(ProductSetBranchView, self).validate_widgets(data)
> +
> +    def validate(self, data):
> +        """See `LaunchpadFormView`."""
> +        # If widget validation returned errors then there is no need to
> +        # continue as we'd likely just override the errors reported there.
> +        if len(self.errors) > 0:
> +            return
> +        branch_type = data.get('branch_type')
> +        self._validateLinkLpGit(data)

This needs to skip Git validation in the series case.

> +        if branch_type == IMPORT_EXTERNAL:
> +            self._validateImportExternal(data)
> +        elif branch_type == LINK_LP_BZR:
> +            self._validateLinkLpBzr(data)
> +        else:
> +            raise AssertionError("Unknown branch type %s" % branch_type)
> +
> +    @property
> +    def target(self):
> +        """The branch target for the context."""
> +        return IBranchTarget(self.context)
> +
> +    def abort_update(self):
> +        """Abort transaction.
> +
> +        This is normally handled by LaunchpadFormView, but this can be called
> +        from the success handler."""
> +
> +        self.errors_in_action = True
> +        self._abort()
> +        return

Pointless return.

> +
> +    def add_update_notification(self):
> +        self.request.response.addInfoNotification(
> +            'Project code updated.')

No code was updated. The settings were changed, though.

> +
> +    @action(_('Update'), name='update')
> +    def update_action(self, action, data):
> +        branch_type = data.get('branch_type')
> +        default_vcs = data.get('default_vcs')
> +

Default VCS and Git settings should be skipped in the series case.

> +        if default_vcs:
> +            self.context.vcs = default_vcs
> +
> +        repo = data.get('git_repository_location')
> +        if repo:
> +            getUtility(IGitRepositorySet).setDefaultRepository(
> +                self.context, repo)

This makes it impossible to unset the default.

> +        if branch_type == LINK_LP_BZR:
> +            branch_location = data.get('branch_location')
> +            if branch_location != self.series.branch:
> +                self.series.branch = branch_location
> +                # Request an initial upload of translation files.
> +                getUtility(IRosettaUploadJobSource).create(
> +                    self.series.branch, NULL_REVISION)
> +            else:
> +                self.series.branch = branch_location
> +            self.add_update_notification()
> +        else:
> +            branch_name = data.get('branch_name')
> +            branch_owner = data.get('branch_owner')
> +
> +            if branch_type == IMPORT_EXTERNAL:
> +                rcs_type = data.get('rcs_type')
> +                if rcs_type == RevisionControlSystems.CVS:
> +                    cvs_root = data.get('repo_url')
> +                    cvs_module = data.get('cvs_module')
> +                    url = None
> +                else:
> +                    cvs_root = None
> +                    cvs_module = None
> +                    url = data.get('repo_url')
> +                rcs_item = RevisionControlSystems.items[rcs_type.name]
> +                try:
> +                    code_import = getUtility(ICodeImportSet).new(
> +                        owner=branch_owner,
> +                        registrant=self.user,
> +                        target=IBranchTarget(self.target),
> +                        branch_name=branch_name,
> +                        rcs_type=rcs_item,
> +                        url=url,
> +                        cvs_root=cvs_root,
> +                        cvs_module=cvs_module)
> +                except BranchExists as e:
> +                    self._setBranchExists(e.existing_branch, 'branch_name')
> +                    self.abort_update()
> +                self.series.branch = code_import.branch
> +                self.request.response.addInfoNotification(
> +                    'Code import created and branch linked to the series.')
> +            else:
> +                raise UnexpectedFormData(branch_type)
> +
> +    def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
> +        """Create a new hosted Bazaar branch.
> +
> +        Return the branch on success or None.
> +        """
> +        branch = None
> +        try:
> +            namespace = self.target.getNamespace(branch_owner)
> +            branch = namespace.createBranch(
> +                branch_type=BranchType.HOSTED, name=branch_name,
> +                registrant=self.user, url=repo_url)
> +        except BranchCreationForbidden:
> +            self.addError(
> +                "You are not allowed to create branches in %s." %
> +                self.context.displayname)
> +        except BranchExists as e:
> +            self._setBranchExists(e.existing_branch, 'branch_name')
> +        if branch is None:
> +            self.errors_in_action = True
> +            # Abort transaction. This is normally handled by
> +            # LaunchpadFormView, but we are already in the success handler.
> +            self._abort()
> +        return branch
> +
> +
>  class ProductRdfView(BaseRdfView):
>      """A view that sets its mime-type to application/rdf+xml"""
>  
> 
> === modified file 'lib/lp/registry/browser/productseries.py'
> --- lib/lp/registry/browser/productseries.py	2015-06-12 14:20:12 +0000
> +++ lib/lp/registry/browser/productseries.py	2015-06-24 08:54:06 +0000
> @@ -28,11 +28,6 @@
>  
>  from operator import attrgetter
>  
> -from bzrlib.revision import NULL_REVISION
> -from lazr.restful.interface import (
> -    copy_field,
> -    use_template,
> -    )
>  from z3c.ptcompat import ViewPageTemplateFile
>  from zope.component import getUtility
>  from zope.formlib import form
> @@ -57,17 +52,11 @@
>      custom_widget,
>      LaunchpadEditFormView,
>      LaunchpadFormView,
> -    render_radio_widget_part,
> -    ReturnToReferrerMixin,
>      )
>  from lp.app.browser.tales import MenuAPI
>  from lp.app.enums import ServiceUsage
> -from lp.app.errors import (
> -    NotFoundError,
> -    UnexpectedFormData,
> -    )
> +from lp.app.errors import NotFoundError
>  from lp.app.interfaces.launchpad import ILaunchpadCelebrities
> -from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
>  from lp.app.widgets.textwidgets import StrippedTextWidget
>  from lp.blueprints.browser.specificationtarget import (
>      HasSpecificationsMenuMixin,
> @@ -81,24 +70,9 @@
>      StructuralSubscriptionTargetTraversalMixin,
>      )
>  from lp.bugs.interfaces.bugtask import IBugTaskSet
> -from lp.code.browser.branch import BranchNameValidationMixin
>  from lp.code.browser.branchref import BranchRef
> -from lp.code.browser.codeimport import validate_import_url
> -from lp.code.enums import (
> -    BranchType,
> -    RevisionControlSystems,
> -    )
> -from lp.code.errors import (
> -    BranchCreationForbidden,
> -    BranchExists,
> -    )
> -from lp.code.interfaces.branch import IBranch
> -from lp.code.interfaces.branchjob import IRosettaUploadJobSource
> +from lp.code.enums import RevisionControlSystems
>  from lp.code.interfaces.branchtarget import IBranchTarget
> -from lp.code.interfaces.codeimport import (
> -    ICodeImport,
> -    ICodeImportSet,
> -    )
>  from lp.registry.browser import (
>      add_subscribe_link,
>      BaseRdfView,
> @@ -110,6 +84,7 @@
>      InvolvedMenu,
>      PillarInvolvementView,
>      )
> +from lp.registry.browser.product import ProductSetBranchView
>  from lp.registry.errors import CannotPackageProprietaryProduct
>  from lp.registry.interfaces.packaging import (
>      IPackaging,
> @@ -117,7 +92,6 @@
>      )
>  from lp.registry.interfaces.productseries import IProductSeries
>  from lp.registry.interfaces.series import SeriesStatus
> -from lp.services.fields import URIField
>  from lp.services.propertycache import cachedproperty
>  from lp.services.webapp import (
>      ApplicationMenu,
> @@ -756,300 +730,25 @@
>          self.next_url = canonical_url(product)
>  
>  
> -LINK_LP_BZR = 'link-lp-bzr'
> -IMPORT_EXTERNAL = 'import-external'
> -
> -
> -BRANCH_TYPE_VOCABULARY = SimpleVocabulary((
> -    SimpleTerm(LINK_LP_BZR, LINK_LP_BZR,
> -               _("Link to a Bazaar branch already on Launchpad")),
> -    SimpleTerm(IMPORT_EXTERNAL, IMPORT_EXTERNAL,
> -               _("Import a branch hosted somewhere else")),
> -    ))
> -
> -
> -class SetBranchForm(Interface):
> -    """The fields presented on the form for setting a branch."""
> -
> -    use_template(ICodeImport, ['cvs_module'])
> -
> -    rcs_type = Choice(title=_("Type of RCS"),
> -        required=False, vocabulary=RevisionControlSystems,
> -        description=_(
> -            "The version control system to import from. "))
> -
> -    repo_url = URIField(
> -        title=_("Branch URL"), required=True,
> -        description=_("The URL of the branch."),
> -        allowed_schemes=["http", "https"],
> -        allow_userinfo=False, allow_port=True, allow_query=False,
> -        allow_fragment=False, trailing_slash=False)
> -
> -    branch_location = copy_field(
> -        IProductSeries['branch'], __name__='branch_location',
> -        title=_('Branch'),
> -        description=_(
> -            "The Bazaar branch for this series in Launchpad, "
> -            "if one exists."))
> -
> -    branch_type = Choice(
> -        title=_('Import type'), vocabulary=BRANCH_TYPE_VOCABULARY,
> -        description=_("The type of import"), required=True)
> -
> -    branch_name = copy_field(
> -        IBranch['name'], __name__='branch_name', title=_('Branch name'),
> -        description=_(''), required=True)
> -
> -    branch_owner = copy_field(
> -        IBranch['owner'], __name__='branch_owner', title=_('Branch owner'),
> -        description=_(''), required=True)
> -
> -
> -class ProductSeriesSetBranchView(ReturnToReferrerMixin, LaunchpadFormView,
> -                                 ProductSeriesView,
> -                                 BranchNameValidationMixin):
> +class ProductSeriesSetBranchView(ProductSetBranchView, ProductSeriesView):
>      """The view to set a branch for the ProductSeries."""
>  
> -    schema = SetBranchForm
> -    # Set for_input to True to ensure fields marked read-only will be editable
> -    # upon creation.
> -    for_input = True
> -
> -    custom_widget('rcs_type', LaunchpadRadioWidget)
> -    custom_widget('branch_type', LaunchpadRadioWidget)
> -
> -    errors_in_action = False
> -
> -    @property
> -    def initial_values(self):
> -        return dict(
> -            rcs_type=RevisionControlSystems.BZR,
> -            branch_type=LINK_LP_BZR,
> -            branch_location=self.context.branch)
> -
> -    @property
> -    def next_url(self):
> -        """Return the next_url.
> -
> -        Use the value from `ReturnToReferrerMixin` or None if there
> -        are errors.
> -        """
> -        if self.errors_in_action:
> -            return None
> -        return super(ProductSeriesSetBranchView, self).next_url
> -
> -    def setUpWidgets(self):
> -        """See `LaunchpadFormView`."""
> -        super(ProductSeriesSetBranchView, self).setUpWidgets()
> -        widget = self.widgets['rcs_type']
> -        vocab = widget.vocabulary
> -        current_value = widget._getFormValue()
> -        self.rcs_type_cvs = render_radio_widget_part(
> -            widget, vocab.CVS, current_value, 'CVS')
> -        self.rcs_type_svn = render_radio_widget_part(
> -            widget, vocab.BZR_SVN, current_value, 'SVN')
> -        self.rcs_type_git = render_radio_widget_part(
> -            widget, vocab.GIT, current_value)
> -        self.rcs_type_bzr = render_radio_widget_part(
> -            widget, vocab.BZR, current_value)
> -        self.rcs_type_emptymarker = widget._emptyMarker()
> -
> -        widget = self.widgets['branch_type']
> -        current_value = widget._getFormValue()
> -        vocab = widget.vocabulary
> -
> -        (self.branch_type_link,
> -         self.branch_type_import) = [
> -            render_radio_widget_part(widget, value, current_value)
> -            for value in (LINK_LP_BZR, IMPORT_EXTERNAL)]
> -
> -    def _validateLinkLpBzr(self, data):
> -        """Validate data for link-lp-bzr case."""
> -        if 'branch_location' not in data:
> -            self.setFieldError(
> -                'branch_location', 'The branch location must be set.')
> -
> -    def _validateImportExternal(self, data):
> -        """Validate data for import external case."""
> -        rcs_type = data.get('rcs_type')
> -        repo_url = data.get('repo_url')
> -
> -        # Private teams are forbidden from owning code imports.
> -        branch_owner = data.get('branch_owner')
> -        if branch_owner is not None and branch_owner.private:
> -            self.setFieldError(
> -                'branch_owner', 'Private teams are forbidden from owning '
> -                'external imports.')
> -
> -        if repo_url is None:
> -            self.setFieldError(
> -                'repo_url', 'You must set the external repository URL.')
> -        else:
> -            reason = validate_import_url(repo_url, rcs_type)
> -            if reason:
> -                self.setFieldError('repo_url', reason)
> -
> -        # RCS type is mandatory.
> -        # This condition should never happen since an initial value is set.
> -        if rcs_type is None:
> -            # The error shows but does not identify the widget.
> -            self.setFieldError(
> -                'rcs_type',
> -                'You must specify the type of RCS for the remote host.')
> -        elif rcs_type == RevisionControlSystems.CVS:
> -            if 'cvs_module' not in data:
> -                self.setFieldError('cvs_module', 'The CVS module must be set.')
> -        self._validateBranch(data)
> -
> -    def _validateBranch(self, data):
> -        """Validate that branch name and owner are set."""
> -        if 'branch_name' not in data:
> -            self.setFieldError('branch_name', 'The branch name must be set.')
> -        if 'branch_owner' not in data:
> -            self.setFieldError('branch_owner', 'The branch owner must be set.')
> -
> -    def _setRequired(self, names, value):
> -        """Mark the widget field as optional."""
> -        for name in names:
> -            widget = self.widgets[name]
> -            # The 'required' property on the widget context is set to False.
> -            # The widget also has a 'required' property but it isn't used
> -            # during validation.
> -            widget.context.required = value
> -
> -    def _validSchemes(self, rcs_type):
> -        """Return the valid schemes for the repository URL."""
> -        schemes = set(['http', 'https'])
> -        # Extend the allowed schemes for the repository URL based on
> -        # rcs_type.
> -        extra_schemes = {
> -            RevisionControlSystems.BZR_SVN: ['svn'],
> -            RevisionControlSystems.GIT: ['git'],
> -            RevisionControlSystems.BZR: ['bzr'],
> -            }
> -        schemes.update(extra_schemes.get(rcs_type, []))
> -        return schemes
> -
> -    def validate_widgets(self, data, names=None):
> -        """See `LaunchpadFormView`."""
> -        names = ['branch_type', 'rcs_type']
> -        super(ProductSeriesSetBranchView, self).validate_widgets(data, names)
> -        branch_type = data.get('branch_type')
> -        if branch_type == LINK_LP_BZR:
> -            # Mark other widgets as non-required.
> -            self._setRequired(['rcs_type', 'repo_url', 'cvs_module',
> -                               'branch_name', 'branch_owner'], False)
> -        elif branch_type == IMPORT_EXTERNAL:
> -            rcs_type = data.get('rcs_type')
> -
> -            # Set the valid schemes based on rcs_type.
> -            self.widgets['repo_url'].field.allowed_schemes = (
> -                self._validSchemes(rcs_type))
> -            # The branch location is not required for validation.
> -            self._setRequired(['branch_location'], False)
> -            # The cvs_module is required if it is a CVS import.
> -            if rcs_type == RevisionControlSystems.CVS:
> -                self._setRequired(['cvs_module'], True)
> -        else:
> -            raise AssertionError("Unknown branch type %s" % branch_type)
> -        # Perform full validation now.
> -        super(ProductSeriesSetBranchView, self).validate_widgets(data)
> -
> -    def validate(self, data):
> -        """See `LaunchpadFormView`."""
> -        # If widget validation returned errors then there is no need to
> -        # continue as we'd likely just override the errors reported there.
> -        if len(self.errors) > 0:
> -            return
> -        branch_type = data['branch_type']
> -        if branch_type == IMPORT_EXTERNAL:
> -            self._validateImportExternal(data)
> -        elif branch_type == LINK_LP_BZR:
> -            self._validateLinkLpBzr(data)
> -        else:
> -            raise AssertionError("Unknown branch type %s" % branch_type)
> +    @property
> +    def is_series(self):
> +        return True

No need for a property.

> +
> +    @property
> +    def series(self):
> +        return self.context
>  
>      @property
>      def target(self):
>          """The branch target for the context."""
>          return IBranchTarget(self.context.product)
>  
> -    @action(_('Update'), name='update')
> -    def update_action(self, action, data):
> -        branch_type = data.get('branch_type')
> -        if branch_type == LINK_LP_BZR:
> -            branch_location = data.get('branch_location')
> -            if branch_location != self.context.branch:
> -                self.context.branch = branch_location
> -                # Request an initial upload of translation files.
> -                getUtility(IRosettaUploadJobSource).create(
> -                    self.context.branch, NULL_REVISION)
> -            else:
> -                self.context.branch = branch_location
> -            self.request.response.addInfoNotification(
> -                'Series code location updated.')
> -        else:
> -            branch_name = data.get('branch_name')
> -            branch_owner = data.get('branch_owner')
> -
> -            if branch_type == IMPORT_EXTERNAL:
> -                rcs_type = data.get('rcs_type')
> -                if rcs_type == RevisionControlSystems.CVS:
> -                    cvs_root = data.get('repo_url')
> -                    cvs_module = data.get('cvs_module')
> -                    url = None
> -                else:
> -                    cvs_root = None
> -                    cvs_module = None
> -                    url = data.get('repo_url')
> -                rcs_item = RevisionControlSystems.items[rcs_type.name]
> -                try:
> -                    code_import = getUtility(ICodeImportSet).new(
> -                        owner=branch_owner,
> -                        registrant=self.user,
> -                        target=IBranchTarget(self.context.product),
> -                        branch_name=branch_name,
> -                        rcs_type=rcs_item,
> -                        url=url,
> -                        cvs_root=cvs_root,
> -                        cvs_module=cvs_module)
> -                except BranchExists as e:
> -                    self._setBranchExists(e.existing_branch, 'branch_name')
> -                    self.errors_in_action = True
> -                    # Abort transaction. This is normally handled
> -                    # by LaunchpadFormView, but we are already in
> -                    # the success handler.
> -                    self._abort()
> -                    return
> -                self.context.branch = code_import.branch
> -                self.request.response.addInfoNotification(
> -                    'Code import created and branch linked to the series.')
> -            else:
> -                raise UnexpectedFormData(branch_type)
> -
> -    def _createBzrBranch(self, branch_name, branch_owner, repo_url=None):
> -        """Create a new hosted Bazaar branch.
> -
> -        Return the branch on success or None.
> -        """
> -        branch = None
> -        try:
> -            namespace = self.target.getNamespace(branch_owner)
> -            branch = namespace.createBranch(
> -                branch_type=BranchType.HOSTED, name=branch_name,
> -                registrant=self.user, url=repo_url)
> -        except BranchCreationForbidden:
> -            self.addError(
> -                "You are not allowed to create branches in %s." %
> -                self.context.displayname)
> -        except BranchExists as e:
> -            self._setBranchExists(e.existing_branch, 'branch_name')
> -        if branch is None:
> -            self.errors_in_action = True
> -            # Abort transaction. This is normally handled by
> -            # LaunchpadFormView, but we are already in the success handler.
> -            self._abort()
> -        return branch
> +    def add_update_notification(self):
> +        self.request.response.addInfoNotification(
> +            'Series code location updated.')
>  
>  
>  class ProductSeriesReviewView(LaunchpadEditFormView):
> 
> === modified file 'lib/lp/registry/browser/tests/test_product.py'
> --- lib/lp/registry/browser/tests/test_product.py	2014-06-19 06:38:53 +0000
> +++ lib/lp/registry/browser/tests/test_product.py	2015-06-24 08:54:06 +0000
> @@ -34,6 +34,7 @@
>  from lp.registry.enums import (
>      EXCLUSIVE_TEAM_POLICY,
>      TeamMembershipPolicy,
> +    VCSType,
>      )
>  from lp.registry.interfaces.product import (
>      IProductSet,
> @@ -309,6 +310,12 @@
>          view = create_initialized_view(self.product, '+index')
>          self.assertTrue(view.show_programming_languages)
>  
> +    def test_show_default_vcs(self):
> +        with person_logged_in(self.product.owner):
> +            self.product.vcs = VCSType.GIT
> +        view = create_initialized_view(self.product, '+index')
> +        self.assertTrue(view.show_vcs)
> +
>      def test_show_license_info_without_other_license(self):
>          # show_license_info is false when one of the "other" licences is
>          # not selected.
> 
> === added file 'lib/lp/registry/browser/tests/test_product_views.py'
> --- lib/lp/registry/browser/tests/test_product_views.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/registry/browser/tests/test_product_views.py	2015-06-24 08:54:06 +0000
> @@ -0,0 +1,35 @@
> +# Copyright 2011-2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""View tests for Product pages."""
> +
> +__metaclass__ = type
> +
> +import soupmatchers
> +from zope.security.proxy import removeSecurityProxy
> +
> +from lp.services.webapp import canonical_url
> +from lp.testing import BrowserTestCase
> +from lp.testing.layers import DatabaseFunctionalLayer
> +
> +
> +class TestProductSetBranchView(BrowserTestCase):
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def getBrowser(self, project, view_name=None):
> +        project = removeSecurityProxy(project)
> +        url = canonical_url(project, view_name=view_name)
> +        return self.getUserBrowser(url, project.owner)
> +
> +    def test_link_existing_git_repository(self):
> +        repo = removeSecurityProxy(self.factory.makeProductGitRepository())
> +        browser = self.getBrowser(repo.project, '+configure-code')
> +        browser.getControl('Git', index=0).click()
> +        browser.getControl('Git Repository').value = repo.shortened_path
> +        browser.getControl('Update').click()
> +
> +        tag = soupmatchers.Tag(
> +            'success-div', 'div', attrs={'class': 'informational message'},
> +             text='Project code updated.')
> +        self.assertThat(browser.contents, soupmatchers.HTMLContains(tag))
> 
> === modified file 'lib/lp/registry/interfaces/product.py'
> --- lib/lp/registry/interfaces/product.py	2015-05-13 05:03:33 +0000
> +++ lib/lp/registry/interfaces/product.py	2015-06-24 08:54:06 +0000
> @@ -761,6 +761,13 @@
>              description=_(
>                  "Version control system for this project's code.")))
>  
> +    inferred_vcs = exported(
> +        TextLine(
> +            title=_("Inferred VCS"),
> +            readonly=True,
> +            description=_(
> +                "Inferred version control system for this project's code.")))

It's not a TextLine but a Choice over the enum.

> +
>      def getAllowedBugInformationTypes():
>          """Get the information types that a bug in this project can have.
>  
> 
> === modified file 'lib/lp/registry/model/product.py'
> --- lib/lp/registry/model/product.py	2015-05-13 05:25:30 +0000
> +++ lib/lp/registry/model/product.py	2015-06-24 08:54:06 +0000
> @@ -116,6 +116,8 @@
>      )
>  from lp.code.enums import BranchType
>  from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
> +from lp.code.interfaces.branchcollection import IBranchCollection
> +from lp.code.interfaces.gitcollection import IGitCollection
>  from lp.code.interfaces.gitrepository import IGitRepositorySet
>  from lp.code.model.branch import Branch
>  from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
> @@ -445,6 +447,21 @@
>          """
>          return None
>  
> +    @property
> +    def inferred_vcs(self):
> +        """Use vcs, otherwise infer from existence of git or bzr branches.
> +
> +        Bzr take precedence over git, if no project vcs set.
> +        """
> +        vcs = None
> +        if self.vcs:
> +            return self.vcs
> +        if not IBranchCollection(self).is_empty():
> +            vcs = VCSType.BZR
> +        elif not IGitCollection(self).is_empty():
> +            vcs = VCSType.GIT
> +        return vcs

The "vcs" local is pointless.

> +
>      @date_next_suggest_packaging.setter  # pyflakes:ignore
>      def date_next_suggest_packaging(self, value):
>          """See `IProduct`
> 
> === modified file 'lib/lp/registry/stories/product/xx-product-development-focus.txt'
> --- lib/lp/registry/stories/product/xx-product-development-focus.txt	2015-06-14 23:48:24 +0000
> +++ lib/lp/registry/stories/product/xx-product-development-focus.txt	2015-06-24 08:54:06 +0000
> @@ -74,7 +74,7 @@
>       (http://launchpad.dev/fooix/+edit)
>      >>> print_involvement_portlet(owner_browser)
>      Code
> -    http://launchpad.dev/fooix/trunk/+setbranch
> +    http://launchpad.dev/fooix/+setbranch
>      Bugs
>      http://launchpad.dev/fooix/+configure-bugtracker
>      Translations
> @@ -85,6 +85,7 @@
>  The owner can specify the development focus branch from the overview page.
>  
>      >>> owner_browser.getLink(url='+setbranch').click()
> +    >>> owner_browser.getControl('Bazaar', index=0).click()
>      >>> owner_browser.getControl(name='field.branch_location').value = (
>      ...     '~eric/fooix/trunk')
>      >>> owner_browser.getControl('Update').click()
> 
> === modified file 'lib/lp/registry/templates/product-index.pt'
> --- lib/lp/registry/templates/product-index.pt	2015-01-29 16:28:30 +0000
> +++ lib/lp/registry/templates/product-index.pt	2015-06-24 08:54:06 +0000
> @@ -129,6 +129,11 @@
>                  <dd tal:content="structure view/languages_edit_widget" />
>                </dl>
>  
> +              <dl id="product-vcs" tal:condition="context/inferred_vcs">
> +                <dt>Version control system:</dt>
> +                <dd tal:content="context/inferred_vcs/title">Git</dd>
> +              </dl>
> +
>                <dl id="licences">
>                  <dt>Licences:</dt>
>                  <dd>
> 
> === removed file 'lib/lp/registry/templates/productseries-setbranch.pt'
> --- lib/lp/registry/templates/productseries-setbranch.pt	2015-05-01 13:18:54 +0000
> +++ lib/lp/registry/templates/productseries-setbranch.pt	1970-01-01 00:00:00 +0000
> @@ -1,125 +0,0 @@
> -<html
> -  xmlns="http://www.w3.org/1999/xhtml";
> -  xmlns:tal="http://xml.zope.org/namespaces/tal";
> -  xmlns:metal="http://xml.zope.org/namespaces/metal";
> -  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> -  metal:use-macro="view/macro:page/main_only"
> -  i18n:domain="launchpad">
> -
> -<body>
> -
> -<metal:block fill-slot="head_epilogue">
> -  <style type="text/css">
> -    .subordinate {
> -      margin: 0.5em 0 0.5em 4em;
> -    }
> -  </style>
> -</metal:block>
> -
> -<div metal:fill-slot="main">
> -
> -  <p id="push-instructions">
> -    You can push a Bazaar branch directly to Launchpad with the command:<br />
> -    <tt class="command">
> -      bzr push lp:~<tal:user replace="view/user/name"/>/<tal:project replace="context/product/name"/>/<tal:series replace="context/name"/>
> -    </tt>
> -    <tal:no-keys condition="not:view/user/sshkeys">
> -      <br/>To authenticate with the Launchpad branch upload service,
> -      you need to
> -      <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
> -        register a SSH key</a>.
> -    </tal:no-keys>
> -  </p>
> -
> -  <div metal:use-macro="context/@@launchpad_form/form">
> -
> -    <metal:formbody fill-slot="widgets">
> -
> -      <table class="form">
> -
> -        <tr>
> -          <td>
> -            <label tal:replace="structure view/branch_type_link">
> -              Link to a Bazaar branch already in Launchpad
> -            </label>
> -            <table class="subordinate">
> -              <tal:widget define="widget nocall:view/widgets/branch_location">
> -                <metal:block use-macro="context/@@launchpad_form/widget_row" />
> -              </tal:widget>
> -            </table>
> -          </td>
> -        </tr>
> -
> -        <tr>
> -          <td>
> -            <label tal:replace="structure view/branch_type_import">
> -              Import a branch hosted somewhere else
> -            </label>
> -            <table class="subordinate">
> -              <tal:widget define="widget nocall:view/widgets/repo_url">
> -                <metal:block use-macro="context/@@launchpad_form/widget_row" />
> -              </tal:widget>
> -
> -              <tr>
> -                <td>
> -                  <label tal:replace="structure view/rcs_type_bzr">
> -                    Bazaar, hosted externally
> -                  </label>
> -                </td>
> -              </tr>
> -
> -              <tr>
> -                <td>
> -                  <label tal:replace="structure view/rcs_type_git">
> -                    Git
> -                  </label>
> -                </td>
> -              </tr>
> -
> -              <tr>
> -                <td>
> -                  <label tal:replace="structure view/rcs_type_svn">
> -                    SVN
> -                  </label>
> -                </td>
> -              </tr>
> -
> -              <tr>
> -                <td>
> -                  <label tal:replace="structure view/rcs_type_cvs">
> -                    CVS
> -                  </label>
> -                  <table class="subordinate">
> -                  <tal:widget define="widget nocall:view/widgets/cvs_module">
> -                    <metal:block use-macro="context/@@launchpad_form/widget_row" />
> -                  </tal:widget>
> -                  </table>
> -                </td>
> -              </tr>
> -
> -            </table>
> -          </td>
> -        </tr>
> -
> -        <tal:widget define="widget nocall:view/widgets/branch_name">
> -          <metal:block use-macro="context/@@launchpad_form/widget_row" />
> -        </tal:widget>
> -        <tal:widget define="widget nocall:view/widgets/branch_owner">
> -          <metal:block use-macro="context/@@launchpad_form/widget_row" />
> -        </tal:widget>
> -
> -      </table>
> -      <input tal:replace="structure view/rcs_type_emptymarker" />
> -
> -    </metal:formbody>
> -  </div>
> -
> -  <script type="text/javascript">
> -    LPJS.use('lp.code.productseries_setbranch', function(Y) {
> -      Y.on('domready', Y.lp.code.productseries_setbranch.setup);
> -    });
> -  </script>
> -
> -</div>
> -</body>
> -</html>
> 
> === modified file 'lib/lp/registry/tests/test_product.py'
> --- lib/lp/registry/tests/test_product.py	2015-05-13 05:03:33 +0000
> +++ lib/lp/registry/tests/test_product.py	2015-06-24 08:54:06 +0000
> @@ -65,6 +65,7 @@
>      SharingPermission,
>      SpecificationSharingPolicy,
>      TeamMembershipPolicy,
> +    VCSType,
>      )
>  from lp.registry.errors import (
>      CannotChangeInformationType,
> @@ -309,6 +310,13 @@
>              [u'trunk', u'active-series'],
>              [series.name for series in active_series])
>  
> +    def test_inferred_vcs(self):
> +        """VCS is inferred correctly from existing branch or repo."""
> +        git_repo = self.factory.makeGitRepository()
> +        bzr_branch = self.factory.makeBranch()
> +        self.assertEqual(VCSType.GIT, git_repo.target.inferred_vcs)
> +        self.assertEqual(VCSType.BZR, bzr_branch.product.inferred_vcs)
> +
>      def test_owner_cannot_be_open_team(self):
>          """Product owners cannot be open teams."""
>          for policy in INCLUSIVE_TEAM_POLICY:
> 
> === modified file 'lib/lp/testing/factory.py'
> --- lib/lp/testing/factory.py	2015-06-15 09:51:32 +0000
> +++ lib/lp/testing/factory.py	2015-06-24 08:54:06 +0000
> @@ -1222,6 +1222,15 @@
>              product = self.makeProduct()
>          return self.makeBranch(product=product, **kwargs)
>  
> +    def makeProductGitRepository(self, project=None, **kwargs):
> +        """Make a git repository on an arbitrary project.
> +
> +        See `makeGitRepository` for more information on arguments.
> +        """
> +        if project is None:
> +            project = self.makeProduct()
> +        return self.makeGitRepository(target=project, **kwargs)

This isn't much shorter than makeGitRepository(target=self.factory.makeProduct()). I don't think it's worth it.

> +
>      def makeAnyBranch(self, **kwargs):
>          """Make a branch without caring about its container.
>  
> 


-- 
https://code.launchpad.net/~blr/launchpad/ui-project-setbranch/+merge/259069
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References