← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/git-sharing-ui into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-sharing-ui into lp:launchpad.

Commit message:
Extend the pillar sharing UI to handle Git repositories.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-sharing-ui/+merge/257735

The sharing UI isn't really ready for Git; although the underlying service works, some of the text is wrong and the JavaScript code doesn't know about Git repositories.  This fixes all of that up.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-sharing-ui into lp:launchpad.
=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2015-02-16 13:01:34 +0000
+++ lib/lp/registry/browser/pillar.py	2015-04-29 10:34:20 +0000
@@ -425,6 +425,8 @@
         cache = IJSONRequestCache(self.request)
         request = get_current_web_service_request()
         branch_data = self._build_branch_template_data(self.branches, request)
+        gitrepository_data = self._build_gitrepository_template_data(
+            self.gitrepositories, request)
         bug_data = self._build_bug_template_data(self.bugtasks, request)
         spec_data = self._build_specification_template_data(
             self.specifications, request)
@@ -439,6 +441,7 @@
         cache.objects['pillar'] = pillar_data
         cache.objects['bugs'] = bug_data
         cache.objects['branches'] = branch_data
+        cache.objects['gitrepositories'] = gitrepository_data
         cache.objects['specifications'] = spec_data
 
     def _loadSharedArtifacts(self):
@@ -476,6 +479,17 @@
                 information_type=branch.information_type.title))
         return branch_data
 
+    def _build_gitrepository_template_data(self, repositories, request):
+        repository_data = []
+        for repository in repositories:
+            repository_data.append(dict(
+                self_link=absoluteURL(repository, request),
+                web_link=canonical_url(repository, path_only_if_possible=True),
+                repository_name=repository.unique_name,
+                repository_id=repository.id,
+                information_type=repository.information_type.title))
+        return repository_data
+
     def _build_bug_template_data(self, bugtasks, request):
         bug_data = []
         for bugtask in bugtasks:

=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
--- lib/lp/registry/browser/tests/test_pillar_sharing.py	2013-06-20 05:50:00 +0000
+++ lib/lp/registry/browser/tests/test_pillar_sharing.py	2015-04-29 10:34:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2012-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test views that manage sharing."""
@@ -21,6 +21,7 @@
 
 from lp.app.enums import InformationType
 from lp.app.interfaces.services import IService
+from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
 from lp.registry.enums import (
     BranchSharingPolicy,
     BugSharingPolicy,
@@ -29,11 +30,13 @@
 from lp.registry.model.pillar import PillarPerson
 from lp.services.config import config
 from lp.services.database.interfaces import IStore
+from lp.services.features.testing import FeatureFixture
 from lp.services.webapp.interfaces import StormRangeFactoryError
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import (
     login_person,
     logout,
+    normalize_whitespace,
     person_logged_in,
     StormStatementRecorder,
     TestCaseWithFactory,
@@ -55,6 +58,7 @@
 
     def setUp(self):
         super(SharingBaseTestCase, self).setUp()
+        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
         self.driver = self.factory.makePerson()
         self.owner = self.factory.makePerson()
         if self.pillar_type == 'distribution':
@@ -75,11 +79,13 @@
         return grantee
 
     def makeArtifactGrantee(self, grantee=None, with_bug=True,
-                            with_branch=False, security=False):
+                            with_branch=False, with_gitrepository=True,
+                            security=False):
         if grantee is None:
             grantee = self.factory.makePerson()
 
         branch = None
+        gitrepository = None
         bug = None
         artifacts = []
 
@@ -90,6 +96,13 @@
             artifacts.append(
                 self.factory.makeAccessArtifact(concrete=branch))
 
+        if with_gitrepository and self.pillar_type == 'product':
+            gitrepository = self.factory.makeGitRepository(
+                target=self.pillar, owner=self.pillar.owner,
+                information_type=InformationType.PRIVATESECURITY)
+            artifacts.append(
+                self.factory.makeAccessArtifact(concrete=gitrepository))
+
         if with_bug:
             if security:
                 owner = self.factory.makePerson()
@@ -120,7 +133,7 @@
     """Test the pillar sharing details view."""
 
     def getPillarPerson(self, person=None, security=False):
-        person = self.makeArtifactGrantee(person, True, True, security)
+        person = self.makeArtifactGrantee(person, True, True, True, security)
         return PillarPerson(self.pillar, person)
 
     def test_view_filters_security_wisely(self):
@@ -160,8 +173,8 @@
             pillarperson.pillar.name, pillarperson.person.name)
         browser = self.getUserBrowser(user=self.owner, url=url)
         self.assertIn(
-            'There are no shared bugs, branches, or blueprints.',
-            browser.contents)
+            'There are no shared bugs, branches, Git repositories, or '
+            'blueprints.', normalize_whitespace(browser.contents))
 
     def test_init_works(self):
         # The view works with a feature flag.
@@ -203,19 +216,28 @@
                 'web_link': canonical_url(branch, path_only_if_possible=True),
                 'self_link': absoluteURL(branch, request),
             }, cache.objects.get('branches')[0])
+            gitrepository = list(view.gitrepositories)[0]
+            self.assertEqual({
+                'repository_id': gitrepository.id,
+                'repository_name': gitrepository.unique_name,
+                'information_type': gitrepository.information_type.title,
+                'web_link': canonical_url(
+                    gitrepository, path_only_if_possible=True),
+                'self_link': absoluteURL(gitrepository, request),
+            }, cache.objects.get('gitrepositories')[0])
 
     def test_view_query_count(self):
         # Test that the view bulk loads artifacts.
         person = self.factory.makePerson()
         for x in range(0, 15):
-            self.makeArtifactGrantee(person, True, True, False)
+            self.makeArtifactGrantee(person, True, True, True, False)
         pillarperson = PillarPerson(self.pillar, person)
 
         # Invalidate the Storm cache and check the query count.
         IStore(self.pillar).invalidate()
         with StormStatementRecorder() as recorder:
             create_initialized_view(pillarperson, '+index')
-        self.assertThat(recorder, HasQueryCount(LessThan(13)))
+        self.assertThat(recorder, HasQueryCount(LessThan(14)))
 
 
 class TestProductSharingDetailsView(

=== modified file 'lib/lp/registry/enums.py'
--- lib/lp/registry/enums.py	2014-04-28 00:18:58 +0000
+++ lib/lp/registry/enums.py	2015-04-29 10:34:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Enums for the Registry app."""
@@ -56,44 +56,46 @@
     PUBLIC = DBItem(1, """
         Public
 
-        Branches are public unless they contain sensitive security
-        information.
+        Branches and Git repositories are public unless they contain
+        sensitive security information.
         """)
 
     PUBLIC_OR_PROPRIETARY = DBItem(2, """
         Public, can be proprietary
 
-        New branches are public, but can be made proprietary later.
+        New branches and Git repositories are public, but can be made
+        proprietary later.
         """)
 
     PROPRIETARY_OR_PUBLIC = DBItem(3, """
         Proprietary, can be public
 
-        New branches are proprietary, but can be made public later. Only
-        people who can see the project's proprietary information can create
-        new branches.
+        New branches and Git repositories are proprietary, but can be made
+        public later. Only people who can see the project's proprietary
+        information can create new branches or Git repositories.
         """)
 
     PROPRIETARY = DBItem(4, """
         Proprietary
 
-        Branches are always proprietary. Only people who can see the
-        project's proprietary information can create new branches.
+        Branches and Git repositories are always proprietary. Only people
+        who can see the project's proprietary information can create new
+        branches or Git repositories.
         """)
 
     EMBARGOED_OR_PROPRIETARY = DBItem(5, """
         Embargoed, can be proprietary
 
-        New branches are embargoed, but can be made proprietary later. Only
-        people who can see the project's proprietary information can create
-        new branches.
+        New branches and Git repositories are embargoed, but can be made
+        proprietary later. Only people who can see the project's proprietary
+        information can create new branches or Git repositories.
         """)
 
     FORBIDDEN = DBItem(6, """
         Forbidden
 
-        No new branches may be created, but existing branches may still be
-        updated.
+        No new branches or Git repositories may be created, but existing
+        branches and Git repositories may still be updated.
         """)
 
 

=== modified file 'lib/lp/registry/javascript/sharing/sharingdetails.js'
--- lib/lp/registry/javascript/sharing/sharingdetails.js	2013-04-09 05:05:39 +0000
+++ lib/lp/registry/javascript/sharing/sharingdetails.js	2015-04-29 10:34:20 +0000
@@ -1,4 +1,4 @@
-/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE).
  *
  * Sharing details widget
@@ -74,6 +74,34 @@
         ].join(' ');
     },
 
+    _gitrepository_details_row_template: function() {
+        return [
+        '<tr id="shared-gitrepository-{{ repository_id }}">',
+        '    <td>',
+        '        <span class="sortkey">sorttable_branchsortkey</span>',
+        '        <a href="{{web_link}}">',
+        '            {{repository_name}}',
+        '        </a>',
+        '    </td>',
+        '    <td class="action-icons nowrap">',
+        '    <span id="remove-gitrepository-{{repository_id}}">',
+        '    <a class="sprite remove action-icon" href="#"',
+        '        title="Unshare Git repository {{repository_name}} with ' +
+            '{{displayname}}"',
+        '        data-self_link="{{self_link}}" ' +
+            'data-name="{{repository_name}}"',
+        '        data-type="gitrepository">Remove</a>',
+        '    </span>',
+        '    </td>',
+        '   <td>',
+        '   <span class="information_type">',
+        '  {{information_type}}',
+        '   </span>',
+        '   </td>',
+        '</tr>'
+        ].join(' ');
+    },
+
     /**
      * If the artifact with id exists in the model, return it.
      * @param artifact_id
@@ -129,6 +157,9 @@
         '{{#branches}}',
         '{{> branch}}',
         '{{/branches}}',
+        '{{#gitrepositories}}',
+        '{{> gitrepository}}',
+        '{{/gitrepositories}}',
         '{{#specifications}}',
         '{{> spec}}',
         '{{/specifications}}',
@@ -151,7 +182,7 @@
     },
 
     // Delete the specified grantees from the table.
-    delete_artifacts: function(bugs, branches, specifications,
+    delete_artifacts: function(bugs, branches, gitrepositories, specifications,
                                all_rows_deleted) {
         var deleted_row_selectors = [];
         var details_table_body = this.get('details_table_body');
@@ -169,6 +200,14 @@
                 deleted_row_selectors.push(selector);
             }
         });
+        Y.Array.each(gitrepositories, function(gitrepository) {
+            var selector = 'tr[id=shared-gitrepository-' +
+                gitrepository.repository_id + ']';
+            var table_row = details_table_body.one(selector);
+            if (Y.Lang.isValue(table_row)) {
+                deleted_row_selectors.push(selector);
+            }
+        });
         Y.Array.each(specifications, function(spec) {
             var selector = 'tr[id=shared-spec-' + spec.id + ']';
             var table_row = details_table_body.one(selector);
@@ -191,7 +230,9 @@
                 details_table_body
                     .appendChild('<tr></tr>')
                     .appendChild('<td colspan="3"></td>')
-                    .setContent("There are no shared bugs, branches, or blueprints.");
+                    .setContent(
+                        "There are no shared bugs, branches, " +
+                        "Git repositories, or blueprints.");
             }
         };
         var anim_duration = this.get('anim_duration');
@@ -228,6 +269,10 @@
             this._branch_details_row_template());
 
         this.set(
+            'gitrepository_details_row_template',
+            this._gitrepository_details_row_template());
+
+        this.set(
             'spec_details_row_template',
             this._spec_details_row_template());
 
@@ -240,15 +285,17 @@
     renderUI: function() {
         // Load the data
         var branches = this.get('branches');
+        var gitrepositories = this.get('gitrepositories');
         var bugs = this.get('bugs');
         var specs = this.get('specifications');
 
         if (bugs.length === 0 && branches.length === 0 &&
-            specs.length === 0 ) {
+            gitrepositories.length === 0 && specs.length === 0 ) {
             return;
         }
         var partials = {
             branch: this.get('branch_details_row_template'),
+            gitrepository: this.get('gitrepository_details_row_template'),
             bug: this.get('bug_details_row_template'),
             spec: this.get('spec_details_row_template')
         };
@@ -257,11 +304,13 @@
             template,
             {
                 branches: branches,
+                gitrepositories: gitrepositories,
                 bugs: bugs,
                 specifications: specs,
                 displayname: this.get('person_name')
             },
             partials);
+        Y.log("cjw html: " + html);
 
         var details_table_body = this.get('details_table_body');
         var table_body_node = Y.Node.create(html);
@@ -294,12 +343,15 @@
         // been removed.
         var existing_bugs = this.get('bugs');
         var existing_branches = this.get('branches');
+        var existing_gitrepositories = this.get('gitrepositories');
         var existing_specifications = this.get('specifications');
         var model_bugs = LP.cache.bugs;
         var model_branches = LP.cache.branches;
+        var model_gitrepositories = LP.cache.gitrepositories;
         var model_specifications = LP.cache.specifications;
         var deleted_bugs = [];
         var deleted_branches = [];
+        var deleted_gitrepositories = [];
         var deleted_specifications = [];
 
         var self = this;
@@ -319,6 +371,15 @@
                 deleted_branches.push(branch);
             }
         });
+        Y.Array.each(existing_gitrepositories, function(gitrepository) {
+            var model_gitrepository =
+                self._get_artifact_from_model(
+                    gitrepository.repository_id, 'repository_id',
+                    model_gitrepositories);
+            if (!Y.Lang.isValue(model_gitrepository)) {
+                deleted_gitrepositories.push(gitrepository);
+            }
+        });
 
         Y.Array.each(existing_specifications, function(spec) {
             var model_specification = self._get_artifact_from_model(
@@ -329,14 +390,19 @@
         });
 
         if (deleted_bugs.length > 0 || deleted_branches.length > 0 ||
+            deleted_gitrepositories.length > 0 ||
             deleted_specifications.length > 0) {
             this.delete_artifacts(
-                deleted_bugs, deleted_branches, deleted_specifications,
-                model_bugs.length === 0 && model_branches.length === 0 && deleted_specifications.length === 0);
+                deleted_bugs, deleted_branches, deleted_gitrepositories,
+                deleted_specifications,
+                model_bugs.length === 0 && model_branches.length === 0 &&
+                model_gitrepositories.length === 0 &&
+                deleted_specifications.length === 0);
         }
 
         this.set('bugs', model_bugs);
         this.set('branches', model_branches);
+        this.set('gitrepositories', model_gitrepositories);
         this.set('specifications', model_specifications);
 
         Y.lp.app.sorttable.SortTable.registerSortKeyFunction(
@@ -360,6 +426,10 @@
                 value: null
             },
 
+            gitrepository_details_row_template: {
+                value: null
+            },
+
             spec_details_row_template: {
                 value: null
             },
@@ -371,6 +441,13 @@
                 setter: clone_data
             },
 
+            gitrepositories: {
+                value: [],
+                // We clone the data passed in so external modifications do not
+                // interfere.
+                setter: clone_data
+            },
+
             bugs: {
                 value: [],
                 // We clone the data passed in so external modifications do not

=== modified file 'lib/lp/registry/javascript/sharing/sharingdetailsview.js'
--- lib/lp/registry/javascript/sharing/sharingdetailsview.js	2012-12-02 03:01:23 +0000
+++ lib/lp/registry/javascript/sharing/sharingdetailsview.js	2015-04-29 10:34:20 +0000
@@ -1,4 +1,4 @@
-/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE).
  *
  * Disclosure infrastructure.
@@ -31,6 +31,7 @@
         var details_table = new ns.SharingDetailsTable({
             bugs: LP.cache.bugs,
             branches: LP.cache.branches,
+            gitrepositories: LP.cache.gitrepositories,
             person_name: LP.cache.grantee.displayname,
             specifications: LP.cache.specifications,
             write_enabled: true
@@ -75,6 +76,15 @@
                     }
                 });
                 break;
+            case 'gitrepository':
+                Y.Array.some(LP.cache.gitrepositories,
+                             function(gitrepository) {
+                    if (gitrepository.self_link === artifact_uri) {
+                        artifact_id = gitrepository.repository_id;
+                        return true;
+                    }
+                });
+                break;
             case 'spec':
                 Y.Array.some(LP.cache.specifications, function(spec) {
                     if (spec.self_link === artifact_uri) {
@@ -181,6 +191,14 @@
                 return true;
             }
         });
+        var gitrepository_data = LP.cache.gitrepositories;
+        Y.Array.some(gitrepository_data, function(gitrepository, index) {
+            if (gitrepository.self_link === artifact_uri) {
+                gitrepository_data.splice(index, 1);
+                self.syncUI();
+                return true;
+            }
+        });
         var spec_data = LP.cache.specifications;
         Y.Array.some(spec_data, function(spec, index) {
             if (spec.self_link === artifact_uri) {
@@ -203,6 +221,7 @@
         var error_handler = this._error_handler(artifact_uri, artifact_type);
         var bugs = [];
         var branches = [];
+        var gitrepositories = [];
         var specifications = [];
         switch (artifact_type) {
             case 'bug':
@@ -211,6 +230,9 @@
             case 'branch':
                 branches = [artifact_uri];
                 break;
+            case 'gitrepository':
+                gitrepositories = [artifact_uri];
+                break;
             case 'spec':
                 specifications = [artifact_uri];
                 break;
@@ -233,6 +255,7 @@
                 grantee: LP.cache.grantee.self_link,
                 bugs: bugs,
                 branches: branches,
+                gitrepositories: gitrepositories,
                 specifications: specifications
             }
         };

=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js'
--- lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js	2013-03-20 03:41:40 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetails.js	2015-04-29 10:34:20 +0000
@@ -1,4 +1,4 @@
-/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE). */
 YUI.add('lp.registry.sharing.sharingdetails.test', function(Y) {
 
@@ -35,6 +35,15 @@
                             branch_name:'lp:~someone/+junk/somebranch',
                             information_type: 'Private'
                         }
+                    ],
+                    gitrepositories: [
+                        {
+                            self_link: 'api/devel/~someone/+git/somerepo',
+                            web_link: '/~someone/+git/somerepo',
+                            repository_id: '2',
+                            repository_name: '~someone/+git/somerepo',
+                            information_type: 'Private'
+                        }
                     ]
                 }
             };
@@ -61,6 +70,7 @@
                 person_name: 'Fred',
                 bugs: window.LP.cache.bugs,
                 branches: window.LP.cache.branches,
+                gitrepositories: window.LP.cache.gitrepositories,
                 write_enabled: true
             }, overrides);
             window.LP.cache.grantee_data = config.grantees;
@@ -124,6 +134,24 @@
                 "sorttable_branchsortkey", this._strip(sortkey));
         },
 
+        // Test that Git repositories are correctly rendered.
+        test_render_gitrepositories: function () {
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            var row = Y.one(
+                '#sharing-table-body tr#shared-gitrepository-2');
+            var web_link = row.one('a');
+            var expected = "~someone/+git/somerepo";
+            var actual_text = web_link.get('text');
+            Assert.areEqual(expected, this._strip(actual_text));
+            var info_type = row.one('.information_type');
+            var info_text = info_type.get('text');
+            Assert.areEqual('Private', this._strip(info_text));
+            var sortkey = row.one('.sortkey').get('text');
+            Assert.areEqual(
+                "sorttable_branchsortkey", this._strip(sortkey));
+        },
+
         // Test that bugs are correctly rendered.
         test_render_bugs: function () {
             this.details_widget = this._create_Widget();
@@ -189,6 +217,9 @@
             var branch_row_selector =
                 '#sharing-table-body tr[id=shared-branch-2]';
             Y.Assert.isNotNull(Y.one(branch_row_selector));
+            var gitrepository_row_selector =
+                '#sharing-table-body tr[id=shared-gitrepository-2]';
+            Y.Assert.isNotNull(Y.one(gitrepository_row_selector));
             // The sorting data is initialised.
             Y.Assert.isTrue(sorttable_init_called);
             sorttable_ns.SortTable.init = orig_init;
@@ -223,6 +254,34 @@
             Y.Assert.isTrue(event_fired);
         },
 
+        // When the Git repository revoke link is clicked, the correct event
+        // is published.
+        test_gitrepository_revoke_click: function() {
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            var event_fired = false;
+            this.details_widget.subscribe(
+                sharing_details.SharingDetailsTable.REMOVE_GRANT,
+                function(e) {
+                    var delete_link = e.details[0];
+                    var artifact_uri = e.details[1];
+                    var artifact_name = e.details[2];
+                    var artifact_type = e.details[3];
+                    Y.Assert.areEqual(
+                        'api/devel/~someone/+git/somerepo',
+                        artifact_uri);
+                    Y.Assert.areEqual('~someone/+git/somerepo', artifact_name);
+                    Y.Assert.areEqual('gitrepository', artifact_type);
+                    Y.Assert.areEqual(delete_link_to_click, delete_link);
+                    event_fired = true;
+                }
+            );
+            var delete_link_to_click =
+                Y.one('#sharing-table-body span[id=remove-gitrepository-2] a');
+            delete_link_to_click.simulate('click');
+            Y.Assert.isTrue(event_fired);
+        },
+
         // The delete_artifacts call removes the specified bugs from the table.
         test_delete_bugs: function() {
             this.details_widget = this._create_Widget();
@@ -230,7 +289,7 @@
             var row_selector = '#sharing-table-body tr[id=shared-bug-2]';
             Y.Assert.isNotNull(Y.one(row_selector));
             this.details_widget.delete_artifacts(
-                [window.LP.cache.bugs[0]], [], false);
+                [window.LP.cache.bugs[0]], [], [], false);
             Y.Assert.isNull(Y.one(row_selector));
         },
 
@@ -242,7 +301,20 @@
             var row_selector = '#sharing-table-body tr[id=shared-branch-2]';
             Y.Assert.isNotNull(Y.one(row_selector));
             this.details_widget.delete_artifacts(
-                [], [window.LP.cache.branches[0]], false);
+                [], [window.LP.cache.branches[0]], [], false);
+            Y.Assert.isNull(Y.one(row_selector));
+        },
+
+        // The delete_artifacts call removes the specified Git repositories
+        // from the table.
+        test_delete_gitrepositories: function() {
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            var row_selector =
+                '#sharing-table-body tr[id=shared-gitrepository-2]';
+            Y.Assert.isNotNull(Y.one(row_selector));
+            this.details_widget.delete_artifacts(
+                [], [], [window.LP.cache.gitrepositories[0]], false);
             Y.Assert.isNull(Y.one(row_selector));
         },
 
@@ -252,9 +324,12 @@
             this.details_widget.render();
             this.details_widget.delete_artifacts(
                 [window.LP.cache.bugs[0]],
-                [window.LP.cache.branches[0]], true);
+                [window.LP.cache.branches[0]],
+                [window.LP.cache.gitrepositories[0]],
+                true);
             Y.Assert.areEqual(
-                'There are no shared bugs, branches, or blueprints.',
+                'There are no shared bugs, branches, Git repositories, or ' +
+                'blueprints.',
                 Y.one('#sharing-table-body tr').get('text'));
         },
 
@@ -286,6 +361,23 @@
             };
             this.details_widget.display_error(2, 'branch', 'An error occurred');
             Y.Assert.isTrue(success);
+        },
+
+        test_display_error_gitrepository: function() {
+            // A Git repository operation error is displayed correctly.
+            this.details_widget = this._create_Widget();
+            this.details_widget.render();
+            var row_gitrepository = Y.one(
+                '#sharing-table-body tr[id=shared-gitrepository-2]');
+            var success = false;
+            Y.lp.app.errors.display_error = function(flash_node, msg) {
+                Y.Assert.areEqual(row_gitrepository, flash_node);
+                Y.Assert.areEqual('An error occurred', msg);
+                success = true;
+            };
+            this.details_widget.display_error(
+                2, 'gitrepository', 'An error occurred');
+            Y.Assert.isTrue(success);
         }
     }));
 

=== modified file 'lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js'
--- lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js	2013-03-20 03:41:40 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_sharingdetailsview.js	2015-04-29 10:34:20 +0000
@@ -1,4 +1,4 @@
-/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+/* Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
  * GNU Affero General Public License version 3 (see the file LICENSE). */
 
 YUI.add('lp.registry.sharing.sharingdetailsview.test', function (Y) {
@@ -31,12 +31,21 @@
                             branch_name:'lp:~someone/+junk/somebranch'
                         }
                     ],
+                    gitrepositories: [
+                        {
+                            self_link: 'api/devel/~someone/+git/somerepo',
+                            web_link: '/~someone/+git/somerepo',
+                            repository_id: '2',
+                            repository_name: '~someone/+git/somerepo'
+                        }
+                    ],
                     specifications: [
                         {
                             id: 2,
                             information_type: "Proprietary",
                             name: "big-project",
-                            self_link: "api/devel/obsolete-junk/+spec/big-project",
+                            self_link:
+                                "api/devel/obsolete-junk/+spec/big-project",
                             web_link: "/obsolete-junk/+spec/big-project"
                         }
                     ],
@@ -109,7 +118,7 @@
             Y.Assert.isTrue(confirmRemove_called);
         },
 
-        // Clicking a bug remove link calls the confirm_grant_removal
+        // Clicking a branch remove link calls the confirm_grant_removal
         // method with the correct parameters.
         test_remove_branch_grant_click: function() {
             this.view = this._create_Widget();
@@ -132,6 +141,28 @@
             Y.Assert.isTrue(confirmRemove_called);
         },
 
+        // Clicking a Git repository remove link calls the
+        // confirm_grant_removal method with the correct parameters.
+        test_remove_gitrepository_grant_click: function() {
+            this.view = this._create_Widget();
+            this.view.render();
+            var confirmRemove_called = false;
+            this.view.confirm_grant_removal = function(
+                    delete_link, artifact_uri, artifact_name, artifact_type) {
+                Y.Assert.areEqual(
+                    'api/devel/~someone/+git/somerepo', artifact_uri);
+                Y.Assert.areEqual('~someone/+git/somerepo', artifact_name);
+                Y.Assert.areEqual('gitrepository', artifact_type);
+                Y.Assert.areEqual(delete_link_to_click, delete_link);
+                confirmRemove_called = true;
+
+            };
+            var delete_link_to_click =
+                Y.one('#sharing-table-body span[id=remove-gitrepository-2] a');
+            delete_link_to_click.simulate('click');
+            Y.Assert.isTrue(confirmRemove_called);
+        },
+
         // Clicking a spec remove link calls the confirm_grant_removal
         // method with the correct parameters.
         test_remove_spec_grant_click: function() {
@@ -273,6 +304,47 @@
             Y.Assert.isTrue(remove_grant_success_called);
         },
 
+        // The perform_remove_grant method makes the expected XHR calls when a
+        // Git repository grant remove link is clicked.
+        test_perform_remove_gitrepository_grant: function() {
+            var mockio = new Y.lp.testing.mockio.MockIo();
+            var lp_client = new Y.lp.client.Launchpad({
+                io_provider: mockio
+            });
+            this.view = this._create_Widget({
+                lp_client: lp_client
+            });
+            this.view.render();
+            var remove_grant_success_called = false;
+            var self = this;
+            this.view.remove_grant_success = function(artifact_uri) {
+                Y.Assert.areEqual(
+                    'api/devel/~someone/+git/somerepo', artifact_uri);
+                remove_grant_success_called = true;
+            };
+            var delete_link =
+                Y.one('#sharing-table-body span[id=remove-gitrepository-2] a');
+            this.view.perform_remove_grant(
+                delete_link, 'api/devel/~someone/+git/somerepo',
+                'gitrepository');
+            Y.Assert.areEqual(
+                '/api/devel/+services/sharing',
+                mockio.last_request.url);
+            var expected_qs = '';
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'ws.op', 'revokeAccessGrants');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'pillar', '/pillar');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'grantee', '~fred');
+            expected_qs = Y.lp.client.append_qs(
+                expected_qs, 'gitrepositories',
+                'api/devel/~someone/+git/somerepo');
+            Y.Assert.areEqual(expected_qs, mockio.last_request.config.data);
+            mockio.last_request.successJSON({});
+            Y.Assert.isTrue(remove_grant_success_called);
+        },
+
         // The remove bug grant callback updates the model and syncs the UI.
         test_remove_bug_grant_success: function() {
             this.view = this._create_Widget({anim_duration: 0});
@@ -306,6 +378,23 @@
             });
         },
 
+        // The remove Git repository grant callback updates the model and
+        // syncs the UI.
+        test_remove_gitrepository_grant_success: function() {
+            this.view = this._create_Widget({anim_duration: 0});
+            this.view.render();
+            var syncUI_called = false;
+            this.view.syncUI = function() {
+                syncUI_called = true;
+            };
+            this.view.remove_grant_success('api/devel/~someone/+git/somerepo');
+            Y.Assert.isTrue(syncUI_called);
+            Y.Array.each(window.LP.cache.gitrepositories,
+                function(gitrepository) {
+                    Y.Assert.areNotEqual(2, gitrepository.repository_id);
+            });
+        },
+
         // The remove specification grant callback updates the model and syncs
         // the UI.
         test_remove_spec_grant_success: function() {
@@ -326,7 +415,7 @@
 
         // XHR calls display errors correctly.
         _assert_error_displayed_on_failure: function(
-                bug_or_branch, invoke_operation) {
+                artifact, invoke_operation) {
             var mockio = new Y.lp.testing.mockio.MockIo();
             var lp_client = new Y.lp.client.Launchpad({
                 io_provider: mockio
@@ -340,7 +429,7 @@
             grantee_table.display_error = function(
                     artifact_id, artifact_type, error_msg) {
                 Y.Assert.areEqual(2, artifact_id);
-                Y.Assert.areEqual(bug_or_branch, artifact_type);
+                Y.Assert.areEqual(artifact, artifact_type);
                 Y.Assert.areEqual(
                     'Server error, please contact an administrator.',
                     error_msg);
@@ -378,6 +467,20 @@
             this._assert_error_displayed_on_failure('branch', invoke_remove);
         },
 
+        // The perform_remove_grant method handles errors correctly with Git
+        // repositories.
+        test_perform_remove_gitrepository_error: function() {
+            var invoke_remove = function(view) {
+            var delete_link =
+                Y.one('#sharing-table-body span[id=remove-gitrepository-2] a');
+                view.perform_remove_grant(
+                    delete_link, 'api/devel/~someone/+git/somerepo',
+                    'gitrepository');
+            };
+            this._assert_error_displayed_on_failure(
+                'gitrepository', invoke_remove);
+        },
+
         // The perform_remove_grant method handles errors correctly with
         // specifications.
         test_perform_remove_spec_error: function() {

=== modified file 'lib/lp/registry/templates/pillar-sharing-details.pt'
--- lib/lp/registry/templates/pillar-sharing-details.pt	2012-12-03 12:49:35 +0000
+++ lib/lp/registry/templates/pillar-sharing-details.pt	2015-04-29 10:34:20 +0000
@@ -29,14 +29,15 @@
       <p>
       <tal:bugs replace="view/shared_bugs_count">0</tal:bugs> bugs,
       <tal:branches replace="view/shared_branches_count">0</tal:branches> branches,
+      <tal:gitrepositories replace="view/shared_gitrepositories_count">0</tal:gitrepositories> Git repositories,
       and  <tal:specifications
       replace="view/shared_specifications_count">0</tal:specifications>
       blueprints shared with <tal:name replace="view/person/displayname">
       grantee</tal:name>.<br />
 
       <tal:is-team condition="view/person/is_team">
-        <tal:members>3</tal:members> team members can view these bugs and
-        branches.
+        <tal:members>3</tal:members> team members can view these bugs,
+        branches, Git repositories, and blueprints.
       </tal:is-team>
       </p>
     </div>
@@ -48,7 +49,7 @@
       <thead>
         <tr>
           <th colspan="2" width="">
-            Subscribed Bug Report, Branch, or Blueprint
+            Subscribed Bug Report, Branch, Git Repository, or Blueprint
           </th>
           <th>
             Information Type
@@ -58,7 +59,8 @@
       <tbody id="sharing-table-body">
           <tr>
               <td colspan="3">
-                  There are no shared bugs, branches, or blueprints.
+                  There are no shared bugs, branches, Git repositories, or
+                  blueprints.
               </td>
           </tr>
       </tbody>

=== modified file 'lib/lp/registry/templates/pillar-sharing.pt'
--- lib/lp/registry/templates/pillar-sharing.pt	2012-12-03 18:42:09 +0000
+++ lib/lp/registry/templates/pillar-sharing.pt	2015-04-29 10:34:20 +0000
@@ -102,7 +102,8 @@
       <p tal:condition="python: info_type == 'Public'"
          tal:content="string:
         Everyone can see ${context/displayname}'s public information. You can
-        choose who can see the private bugs, branches and blueprints.">
+        choose who can see the private bugs, branches, Git repositories, and
+        blueprints.">
      </p>
      <p tal:condition="python: info_type != 'Public'"
        tal:content="string: This project has no public information.">


Follow ups