← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/codeimport-git-read-only-views into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/codeimport-git-read-only-views into lp:launchpad with lp:~cjwatson/launchpad/git-code-import-security-deletion as a prerequisite.

Commit message:
Add read-only views for Git-targeted code imports.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1469459 in Launchpad itself: "import external code into a LP git repo (natively)"
  https://bugs.launchpad.net/launchpad/+bug/1469459

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/codeimport-git-read-only-views/+merge/308374

We don't have much in the way of tests for this yet, but that will be easier to bring up once we have creation webservice/view handling in place.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/codeimport-git-read-only-views into lp:launchpad.
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py	2016-05-07 00:40:18 +0000
+++ lib/lp/code/browser/branch.py	2016-10-13 12:55:20 +0000
@@ -89,12 +89,12 @@
     latest_proposals_for_each_branch,
     )
 from lp.code.browser.branchref import BranchRef
+from lp.code.browser.codeimport import CodeImportTargetMixin
 from lp.code.browser.decorations import DecoratedBranch
 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
 from lp.code.browser.widgets.branchtarget import BranchTargetWidget
 from lp.code.enums import (
     BranchType,
-    CodeImportResultStatus,
     CodeImportReviewStatus,
     )
 from lp.code.errors import (
@@ -403,7 +403,7 @@
 
 
 class BranchView(InformationTypePortletMixin, FeedsMixin, BranchMirrorMixin,
-                 LaunchpadView, HasSnapsViewMixin):
+                 LaunchpadView, HasSnapsViewMixin, CodeImportTargetMixin):
 
     feed_types = (
         BranchFeedLink,
@@ -596,31 +596,6 @@
         return collection.getExtendedRevisionDetails(
             self.user, self.context.latest_revisions)
 
-    @cachedproperty
-    def latest_code_import_results(self):
-        """Return the last 10 CodeImportResults."""
-        return list(self.context.code_import.results[:10])
-
-    def iconForCodeImportResultStatus(self, status):
-        """The icon to represent the `CodeImportResultStatus` `status`."""
-        if status == CodeImportResultStatus.SUCCESS_PARTIAL:
-            return "/@@/yes-gray"
-        elif status in CodeImportResultStatus.successes:
-            return "/@@/yes"
-        else:
-            return "/@@/no"
-
-    @property
-    def url_is_web(self):
-        """True if an imported branch's URL is HTTP or HTTPS."""
-        # You should only be calling this if it's an SVN, BZR or GIT code
-        # import
-        assert self.context.code_import
-        url = self.context.code_import.url
-        assert url
-        # https starts with http too!
-        return url.startswith("http")
-
     @property
     def show_merge_links(self):
         """Return whether or not merge proposal links should be shown.
@@ -799,7 +774,7 @@
                 target is not None and target != self.context.target):
                 try:
                     self.context.setTarget(self.user, project=target)
-                except BranchTargetError, e:
+                except BranchTargetError as e:
                     self.setFieldError('target', e.message)
                     return
 

=== modified file 'lib/lp/code/browser/codeimport.py'
--- lib/lp/code/browser/codeimport.py	2016-10-06 15:59:34 +0000
+++ lib/lp/code/browser/codeimport.py	2016-10-13 12:55:20 +0000
@@ -12,6 +12,7 @@
     'CodeImportSetBreadcrumb',
     'CodeImportSetNavigation',
     'CodeImportSetView',
+    'CodeImportTargetMixin',
     'validate_import_url',
     ]
 
@@ -49,6 +50,7 @@
 from lp.code.enums import (
     BranchSubscriptionDiffSize,
     BranchSubscriptionNotificationLevel,
+    CodeImportResultStatus,
     CodeImportReviewStatus,
     CodeReviewNotificationLevel,
     NON_CVS_RCS_TYPES,
@@ -191,8 +193,8 @@
                 self.addError(structured("""
                     Those CVS details are already specified for
                     the imported branch <a href="%s">%s</a>.""",
-                    canonical_url(code_import.branch),
-                    code_import.branch.unique_name))
+                    canonical_url(code_import.target),
+                    code_import.target.unique_name))
 
     def _validateURL(self, url, rcs_type, existing_import=None,
                      field_name='url'):
@@ -495,12 +497,12 @@
 class CodeImportEditView(CodeImportBaseView):
     """View for editing code imports.
 
-    This view is registered against the branch, but mostly edits the code
-    import for that branch -- the exception being that it also allows the
-    editing of the branch whiteboard.  If the branch has no associated code
-    import, then the result is a 404.  If the branch does have a code import,
-    then the adapters property allows the form internals to do the associated
-    mappings.
+    This view is registered against the target, but mostly edits the code
+    import for that target -- the exception being that it also allows the
+    editing of the branch whiteboard in the case of Bazaar branches.  If the
+    target has no associated code import, then the result is a 404.  If the
+    target does have a code import, then the adapters property allows the
+    form internals to do the associated mappings.
     """
 
     schema = EditCodeImportForm
@@ -513,16 +515,20 @@
 
     @property
     def initial_values(self):
-        return {'whiteboard': self.context.whiteboard}
+        if (self.code_import.target_rcs_type ==
+                TargetRevisionControlSystems.BZR):
+            return {'whiteboard': self.context.whiteboard}
+        else:
+            return {}
 
     def initialize(self):
-        """Show a 404 if the branch has no code import."""
+        """Show a 404 if the target has no code import."""
         self.code_import = self.context.code_import
         if self.code_import is None:
             raise NotFoundError
         if not self._super_user:
             raise Unauthorized
-        # The next and cancel location is the branch details page.
+        # The next and cancel location is the target details page.
         self.cancel_url = self.next_url = canonical_url(self.context)
         super(CodeImportEditView, self).initialize()
 
@@ -543,6 +549,10 @@
         else:
             raise AssertionError('Unknown rcs_type for code import.')
 
+        if (self.code_import.target_rcs_type ==
+                TargetRevisionControlSystems.GIT):
+            self.form_fields = self.form_fields.omit('whiteboard')
+
     def _showButtonForStatus(self, status):
         """If the status is different, and the user is super, show button."""
         return self._super_user and self.code_import.review_status != status
@@ -604,3 +614,32 @@
             "This foreign branch URL is already specified for the imported "
             "branch <a href='%s'>%s</a>.", canonical_url(code_import.branch),
             code_import.branch.unique_name)
+
+
+class CodeImportTargetMixin:
+    """Common code import methods for Branch and GitRepository views."""
+
+    @cachedproperty
+    def latest_code_import_results(self):
+        """Return the last 10 CodeImportResults."""
+        return list(self.context.code_import.results[:10])
+
+    def iconForCodeImportResultStatus(self, status):
+        """The icon to represent the `CodeImportResultStatus` `status`."""
+        if status == CodeImportResultStatus.SUCCESS_PARTIAL:
+            return "/@@/yes-gray"
+        elif status in CodeImportResultStatus.successes:
+            return "/@@/yes"
+        else:
+            return "/@@/no"
+
+    @property
+    def url_is_web(self):
+        """True if an imported branch's URL is HTTP or HTTPS."""
+        # You should only be calling this if it's an SVN, BZR or GIT code
+        # import
+        assert self.context.code_import
+        url = self.context.code_import.url
+        assert url
+        # https starts with http too!
+        return url.startswith("http")

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2016-06-25 08:05:06 +0000
+++ lib/lp/code/browser/configure.zcml	2016-10-13 12:55:20 +0000
@@ -629,7 +629,7 @@
             permission="launchpad.AnyPerson"/>
     <browser:url
         for="lp.code.interfaces.codeimport.ICodeImport"
-        attribute_to_parent="branch"
+        attribute_to_parent="target"
         path_expression="string:+code-import"
         rootsite="code"/>
     <browser:navigation
@@ -815,6 +815,9 @@
         <browser:page
             name="++repository-recipes"
             template="../templates/gitrepository-recipes.pt"/>
+        <browser:page
+            name="++repository-import-details"
+            template="../templates/gitrepository-import-details.pt"/>
     </browser:pages>
     <browser:page
         for="lp.code.interfaces.gitrepository.IGitRepository"

=== modified file 'lib/lp/code/browser/gitrepository.py'
--- lib/lp/code/browser/gitrepository.py	2016-10-05 10:07:08 +0000
+++ lib/lp/code/browser/gitrepository.py	2016-10-13 12:55:20 +0000
@@ -53,6 +53,7 @@
 from lp.app.vocabularies import InformationTypeVocabulary
 from lp.app.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription
 from lp.code.browser.branch import CodeEditOwnerMixin
+from lp.code.browser.codeimport import CodeImportTargetMixin
 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
 from lp.code.browser.widgets.gitrepositorytarget import (
     GitRepositoryTargetDisplayWidget,
@@ -191,6 +192,11 @@
             return None
         return GitRepositoryDiffView(self.context, self.request, old, new)
 
+    @stepto("+code-import")
+    def traverse_code_import(self):
+        """Traverses to the `ICodeImport` for the repository."""
+        return self.context.code_import
+
 
 class GitRepositoryEditMenu(NavigationMenu):
     """Edit menu for `IGitRepository`."""
@@ -294,7 +300,7 @@
 
 
 class GitRepositoryView(InformationTypePortletMixin, LaunchpadView,
-                        HasSnapsViewMixin):
+                        HasSnapsViewMixin, CodeImportTargetMixin):
 
     related_features = {
         "code.git.recipes.enabled": False,

=== modified file 'lib/lp/code/browser/tests/test_codeimport.py'
--- lib/lp/code/browser/tests/test_codeimport.py	2014-02-24 07:19:52 +0000
+++ lib/lp/code/browser/tests/test_codeimport.py	2016-10-13 12:55:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the code import browser code."""
@@ -7,9 +7,14 @@
 
 import re
 
+from testtools.matchers import StartsWith
 from zope.security.interfaces import Unauthorized
 
-from lp.code.enums import RevisionControlSystems
+from lp.code.enums import (
+    RevisionControlSystems,
+    TargetRevisionControlSystems,
+    )
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.webapp import canonical_url
 from lp.testing import (
     person_logged_in,
@@ -27,28 +32,43 @@
 
     layer = DatabaseFunctionalLayer
 
-    def assertSvnDetailsDisplayed(self, svn_details, rcs_type):
-        """Assert the `svn_details` tag describes a Subversion import.
+    def assertImportDetailsDisplayed(self, code_import, details_id,
+                                     prefix_text, span_title=None):
+        """A code import has its details displayed properly.
 
-        :param svn_details: The BeautifulSoup object for the
-            'svn-import-details' area.
-        :param rcs_type: SVN or BZR_SVN from RevisionControlSystems.
+        :param code_import: An `ICodeImport`.
+        :param details_id: The HTML tag id to search for.
+        :param prefix_text: An expected prefix of the details text.
+        :param span_title: If present, the expected contents of a span title
+            attribute.
         """
-        self.assertEquals(rcs_type.title, svn_details.span['title'])
-        text = re.sub('\s+', ' ', extract_text(svn_details))
-        self.assertTrue(
-            text.startswith(
-                'This branch is an import of the Subversion branch'))
+        browser = self.getUserBrowser(canonical_url(code_import.target))
+        details = find_tag_by_id(browser.contents, details_id)
+        if span_title is not None:
+            self.assertEqual(span_title, details.span['title'])
+        text = re.sub('\s+', ' ', extract_text(details))
+        self.assertThat(text, StartsWith(prefix_text))
 
     def test_bzr_svn_import(self):
-        # The branch page for a bzr-svn-imported branch contains
+        # The branch page for a bzr-svn-imported branch contains a summary
+        # of the import details.
+        code_import = self.factory.makeCodeImport(
+            rcs_type=RevisionControlSystems.BZR_SVN)
+        self.assertImportDetailsDisplayed(
+            code_import, 'svn-import-details',
+            'This branch is an import of the Subversion branch',
+            span_title=RevisionControlSystems.BZR_SVN.title)
+
+    def test_git_to_git_import(self):
+        # The repository page for a git-to-git-imported repository contains
         # a summary of the import details.
-        bzr_svn_import = self.factory.makeCodeImport(
-            rcs_type=RevisionControlSystems.BZR_SVN)
-        browser = self.getUserBrowser(canonical_url(bzr_svn_import.branch))
-        svn_details = find_tag_by_id(browser.contents, 'svn-import-details')
-        self.assertSvnDetailsDisplayed(
-            svn_details, RevisionControlSystems.BZR_SVN)
+        self.useFixture(GitHostingFixture())
+        code_import = self.factory.makeCodeImport(
+            rcs_type=RevisionControlSystems.GIT,
+            target_rcs_type=TargetRevisionControlSystems.GIT)
+        self.assertImportDetailsDisplayed(
+            code_import, 'git-import-details',
+            'This repository is an import of the Git repository')
 
     def test_branch_owner_of_import_forbidden(self):
         # Unauthorized users are forbidden to edit an import.

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2016-10-07 15:50:10 +0000
+++ lib/lp/code/configure.zcml	2016-10-13 12:55:20 +0000
@@ -642,6 +642,7 @@
                        series
                        review_status
                        rcs_type
+                       target_rcs_type
                        cvs_root
                        cvs_module
                        url

=== modified file 'lib/lp/code/interfaces/codeimport.py'
--- lib/lp/code/interfaces/codeimport.py	2016-10-03 17:00:56 +0000
+++ lib/lp/code/interfaces/codeimport.py	2016-10-13 12:55:20 +0000
@@ -41,6 +41,7 @@
 from lp.code.enums import (
     CodeImportReviewStatus,
     RevisionControlSystems,
+    TargetRevisionControlSystems,
     )
 from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.gitrepository import IGitRepository
@@ -112,11 +113,16 @@
             description=_("Only reviewed imports are processed.")))
 
     rcs_type = exported(
-        Choice(title=_("Type of RCS"), readonly=True,
+        Choice(
+            title=_("Type of RCS"), readonly=True,
             required=True, vocabulary=RevisionControlSystems,
-            description=_(
-                "The version control system to import from. "
-                "Can be CVS or Subversion.")))
+            description=_("The revision control system to import from.")))
+
+    target_rcs_type = exported(
+        Choice(
+            title=_("Type of target RCS"), readonly=True,
+            required=True, vocabulary=TargetRevisionControlSystems,
+            description=_("The revision control system to import to.")))
 
     url = exported(
         URIField(title=_("URL"), required=False, readonly=True,

=== modified file 'lib/lp/code/model/codeimport.py'
--- lib/lp/code/model/codeimport.py	2016-10-11 14:26:31 +0000
+++ lib/lp/code/model/codeimport.py	2016-10-13 12:55:20 +0000
@@ -121,6 +121,13 @@
     rcs_type = EnumCol(schema=RevisionControlSystems,
         notNull=False, default=None)
 
+    @property
+    def target_rcs_type(self):
+        if self.branch is not None:
+            return TargetRevisionControlSystems.BZR
+        else:
+            return TargetRevisionControlSystems.GIT
+
     cvs_root = StringCol(default=None)
 
     cvs_module = StringCol(default=None)

=== modified file 'lib/lp/code/templates/branch-index.pt'
--- lib/lp/code/templates/branch-index.pt	2015-09-17 12:13:31 +0000
+++ lib/lp/code/templates/branch-index.pt	2016-10-13 12:55:20 +0000
@@ -113,7 +113,7 @@
        tal:condition="context/branch_type/enumvalue:IMPORTED">
     <div class="portlet">
       <h2>Import details</h2>
-      <tal:branch-pending-merges
+      <tal:branch-import-details
           replace="structure context/@@++branch-import-details" />
     </div>
   </div>

=== modified file 'lib/lp/code/templates/codeimport-machine-index.pt'
--- lib/lp/code/templates/codeimport-machine-index.pt	2009-08-18 00:25:14 +0000
+++ lib/lp/code/templates/codeimport-machine-index.pt	2016-10-13 12:55:20 +0000
@@ -42,7 +42,7 @@
         <table class="simple">
           <tal:jobs repeat="job jobs">
             <tr>
-              <td><tal:branch replace="structure job/code_import/branch/fmt:link"/></td>
+              <td><tal:target replace="structure job/code_import/target/fmt:link"/></td>
               <td>Started: <tal:started replace="job/date_started/fmt:approximatedate"/></td>
               <td>Last heartbeat: <tal:heartbeat replace="job/heartbeat/fmt:approximatedate"/></td>
             </tr>
@@ -71,7 +71,7 @@
             </tal:has-person>
             <tal:event replace="event/event_type/title" />
             <tal:code-import condition="event/code_import"
-                             replace="structure event/code_import/branch/fmt:link"/>
+                             replace="structure event/code_import/target/fmt:link"/>
           </dt>
           <dd tal:condition="event/items">
             <tal:items repeat="item event/items">

=== modified file 'lib/lp/code/templates/codeimport-machines.pt'
--- lib/lp/code/templates/codeimport-machines.pt	2009-08-18 00:22:50 +0000
+++ lib/lp/code/templates/codeimport-machines.pt	2016-10-13 12:55:20 +0000
@@ -40,9 +40,9 @@
                 <table class="simple">
                   <tal:jobs repeat="job jobs">
                     <tr>
-                      <td><tal:branch replace="structure job/code_import/branch/fmt:link"/></td>
-                      <td>Started: <tal:branch replace="job/date_started/fmt:approximatedate"/></td>
-                      <td>Last heartbeat: <tal:branch replace="job/heartbeat/fmt:approximatedate"/></td>
+                      <td><tal:target replace="structure job/code_import/target/fmt:link"/></td>
+                      <td>Started: <tal:started replace="job/date_started/fmt:approximatedate"/></td>
+                      <td>Last heartbeat: <tal:heartbeat replace="job/heartbeat/fmt:approximatedate"/></td>
                     </tr>
                   </tal:jobs>
                 </table>

=== added file 'lib/lp/code/templates/gitrepository-import-details.pt'
--- lib/lp/code/templates/gitrepository-import-details.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/gitrepository-import-details.pt	2016-10-13 12:55:20 +0000
@@ -0,0 +1,95 @@
+<div
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  tal:define="context_menu view/context/menu:context">
+
+  <tal:imported-repository
+      condition="context/repository_type/enumvalue:IMPORTED">
+    <div id="import-details" tal:define="repository context;
+                                         code_import repository/code_import">
+      <tal:has-codeimport condition="repository/code_import"
+                          define="code_import repository/code_import">
+
+        <div><strong>Import Status:</strong>
+          <span tal:attributes="class string:codeimport${code_import/review_status/name}"
+                tal:content="code_import/review_status/title"/>
+        </div>
+
+        <tal:git-import condition="code_import/rcs_type/enumvalue:GIT">
+          <p id="git-import-details">
+            This repository is an import of the Git repository at
+            <tal:is-web-url condition="view/url_is_web">
+              <a tal:attributes="href code_import/url"
+                 tal:content="code_import/url" />.
+            </tal:is-web-url>
+            <tal:not-web-url condition="not: view/url_is_web">
+              <span tal:replace="code_import/url" />.
+            </tal:not-web-url>
+          </p>
+        </tal:git-import>
+
+        <tal:has-job define="job code_import/import_job"
+                     condition="job">
+          <div>
+          <tal:is-running condition="job/state/enumvalue:RUNNING">
+            An import is currently running on
+            <tal:machine content="structure job/machine/fmt:link" />,
+            and was started
+            <tal:date-started replace="job/date_started/fmt:displaydate">
+              2 hours ago
+            </tal:date-started>.
+            <tal:is-logtail condition="job/logtail">
+              The last few lines of the job's output were:
+              <div class="logtail">
+                <tal:logtail content="structure job/logtail/fmt:nice_pre" />
+              </div>
+            </tal:is-logtail>
+          </tal:is-running>
+          <tal:not-running condition="not: job/state/enumvalue:RUNNING">
+            The next import is scheduled to run
+            <tal:overdue condition="job/isOverdue">
+              as soon as possible<tal:requested-by
+                condition="job/requesting_user">
+                (requested by
+                <tal:requested-by-user
+                   replace="structure job/requesting_user/fmt:link">
+                  Some user.
+                </tal:requested-by-user>)</tal:requested-by>.
+            </tal:overdue>
+            <tal:not-overdue condition="not: job/isOverdue">
+              <tal:date-started replace="job/date_due/fmt:displaydate">
+                in 2 hours
+              </tal:date-started>.
+            </tal:not-overdue>
+          </tal:not-running>
+          </div>
+        </tal:has-job>
+
+        <tal:failing condition="code_import/review_status/enumvalue:FAILING">
+          <div id="failing-try-again" class="message warning">
+            The import has been suspended because it failed
+            <tal:failure-limit content="modules/lp.services.config/config/codeimport/consecutive_failure_limit"/>
+            or more times in succession.
+          </div>
+        </tal:failing>
+
+        <tal:last-successful condition="code_import/date_last_successful">
+          <p>
+            Last successful import was
+            <tal:last-successful replace="code_import/date_last_successful/fmt:displaydate">
+              2 hours ago
+            </tal:last-successful>.
+          </p>
+        </tal:last-successful>
+
+        <div id="import-results" tal:condition="view/latest_code_import_results">
+          <tal:result repeat="result view/latest_code_import_results">
+            <metal:result use-macro="code_import/@@+macros/show_result"/>
+          </tal:result>
+        </div>
+      </tal:has-codeimport>
+    </div>
+  </tal:imported-repository>
+
+</div>

=== modified file 'lib/lp/code/templates/gitrepository-index.pt'
--- lib/lp/code/templates/gitrepository-index.pt	2016-01-12 15:10:57 +0000
+++ lib/lp/code/templates/gitrepository-index.pt	2016-10-13 12:55:20 +0000
@@ -54,6 +54,16 @@
     </div>
   </div>
 
+  <div id="repository-import-details"
+       class="yui-g"
+       tal:condition="context/repository_type/enumvalue:IMPORTED">
+    <div class="portlet">
+      <h2>Import details</h2>
+      <tal:repository-import-details
+          replace="structure context/@@++repository-import-details" />
+    </div>
+  </div>
+
   <div class="yui-g">
     <div id="repository-branches" class="portlet"
          tal:define="branches view/branches">


Follow ups