Commit 8657e327 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'api-shared-projects' into 'master'

Api shared projects

## What does this MR do?

Exposes the shared projects in the group endpoint

## What are the relevant issue numbers?

Builds upon !5148 and closes #18780

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [x] API support added
- Tests
  - [x] Added for this feature/bug
  - [x] All builds are passing
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if you do - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)

See merge request !5150
parents 7dba7698 33124b4b
...@@ -28,6 +28,7 @@ v 8.10.0 (unreleased) ...@@ -28,6 +28,7 @@ v 8.10.0 (unreleased)
- Wildcards for protected branches. !4665 - Wildcards for protected branches. !4665
- Allow importing from Github using Personal Access Tokens. (Eric K Idema) - Allow importing from Github using Personal Access Tokens. (Eric K Idema)
- API: Todos !3188 (Robert Schilling) - API: Todos !3188 (Robert Schilling)
- API: Expose shared groups for projects and shared projects for groups !5050 (Robert Schilling)
- Add "Enabled Git access protocols" to Application Settings - Add "Enabled Git access protocols" to Application Settings
- Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
- PipelinesFinder uses git cache data - PipelinesFinder uses git cache data
......
...@@ -42,46 +42,49 @@ Parameters: ...@@ -42,46 +42,49 @@ Parameters:
```json ```json
[ [
{ {
"id": 4, "id": 9,
"description": null, "description": "foo",
"default_branch": "master", "default_branch": "master",
"tag_list": [],
"public": false, "public": false,
"visibility_level": 0, "archived": false,
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", "visibility_level": 10,
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
"web_url": "http://example.com/diaspora/diaspora-client", "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
"tag_list": [ "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
"example", "name": "Html5 Boilerplate",
"disapora client" "name_with_namespace": "Experimental / Html5 Boilerplate",
], "path": "html5-boilerplate",
"owner": { "path_with_namespace": "h5bp/html5-boilerplate",
"id": 3,
"name": "Diaspora",
"created_at": "2013-09-30T13: 46: 02Z"
},
"name": "Diaspora Client",
"name_with_namespace": "Diaspora / Diaspora Client",
"path": "diaspora-client",
"path_with_namespace": "diaspora/diaspora-client",
"issues_enabled": true, "issues_enabled": true,
"merge_requests_enabled": true, "merge_requests_enabled": true,
"builds_enabled": true,
"wiki_enabled": true, "wiki_enabled": true,
"snippets_enabled": false, "builds_enabled": true,
"created_at": "2013-09-30T13: 46: 02Z", "snippets_enabled": true,
"last_activity_at": "2013-09-30T13: 46: 02Z", "created_at": "2016-04-05T21:40:50.169Z",
"creator_id": 3, "last_activity_at": "2016-04-06T16:52:08.432Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": { "namespace": {
"created_at": "2013-09-30T13: 46: 02Z", "id": 5,
"description": "", "name": "Experimental",
"id": 3, "path": "h5bp",
"name": "Diaspora", "owner_id": null,
"owner_id": 1, "created_at": "2016-04-05T21:40:49.152Z",
"path": "diaspora", "updated_at": "2016-04-07T08:07:48.466Z",
"updated_at": "2013-09-30T13: 46: 02Z" "description": "foo",
"avatar": {
"url": null
}, },
"archived": false, "share_with_group_lock": false,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png" "visibility_level": 10
},
"avatar_url": null,
"star_count": 1,
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
"shared_with_groups": []
} }
] ]
``` ```
...@@ -96,7 +99,180 @@ GET /groups/:id ...@@ -96,7 +99,180 @@ GET /groups/:id
Parameters: Parameters:
- `id` (required) - The ID or path of a group | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or path of a group |
```bash
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
```
Example response:
```json
{
"id": 4,
"name": "Twitter",
"path": "twitter",
"description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
"visibility_level": 20,
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter",
"projects": [
{
"id": 7,
"description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
"default_branch": "master",
"tag_list": [],
"public": true,
"archived": false,
"visibility_level": 20,
"ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
"http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
"web_url": "https://gitlab.example.com/twitter/typeahead-js",
"name": "Typeahead.Js",
"name_with_namespace": "Twitter / Typeahead.Js",
"path": "typeahead-js",
"path_with_namespace": "twitter/typeahead-js",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": true,
"created_at": "2016-06-17T07:47:25.578Z",
"last_activity_at": "2016-06-17T07:47:25.881Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 4,
"name": "Twitter",
"path": "twitter",
"owner_id": null,
"created_at": "2016-06-17T07:47:24.216Z",
"updated_at": "2016-06-17T07:47:24.216Z",
"description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
"avatar": {
"url": null
},
"share_with_group_lock": false,
"visibility_level": 20
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"open_issues_count": 3,
"public_builds": true,
"shared_with_groups": []
},
{
"id": 6,
"description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 10,
"ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
"http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
"web_url": "https://gitlab.example.com/twitter/flight",
"name": "Flight",
"name_with_namespace": "Twitter / Flight",
"path": "flight",
"path_with_namespace": "twitter/flight",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": true,
"created_at": "2016-06-17T07:47:24.661Z",
"last_activity_at": "2016-06-17T07:47:24.838Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 4,
"name": "Twitter",
"path": "twitter",
"owner_id": null,
"created_at": "2016-06-17T07:47:24.216Z",
"updated_at": "2016-06-17T07:47:24.216Z",
"description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
"avatar": {
"url": null
},
"share_with_group_lock": false,
"visibility_level": 20
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"open_issues_count": 8,
"public_builds": true,
"shared_with_groups": []
}
],
"shared_projects": [
{
"id": 8,
"description": "Velit eveniet provident fugiat saepe eligendi autem.",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 0,
"ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
"http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
"web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
"name": "Html5 Boilerplate",
"name_with_namespace": "H5bp / Html5 Boilerplate",
"path": "html5-boilerplate",
"path_with_namespace": "h5bp/html5-boilerplate",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"container_registry_enabled": true,
"created_at": "2016-06-17T07:47:27.089Z",
"last_activity_at": "2016-06-17T07:47:27.310Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 5,
"name": "H5bp",
"path": "h5bp",
"owner_id": null,
"created_at": "2016-06-17T07:47:26.621Z",
"updated_at": "2016-06-17T07:47:26.621Z",
"description": "Id consequatur rem vel qui doloremque saepe.",
"avatar": {
"url": null
},
"share_with_group_lock": false,
"visibility_level": 20
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"open_issues_count": 4,
"public_builds": true,
"shared_with_groups": [
{
"group_id": 4,
"group_name": "Twitter",
"group_access_level": 30
},
{
"group_id": 3,
"group_name": "Gitlab Org",
"group_access_level": 10
}
]
}
]
}
```
## New group ## New group
...@@ -201,7 +377,8 @@ Example response: ...@@ -201,7 +377,8 @@ Example response:
"star_count": 1, "star_count": 1,
"forks_count": 0, "forks_count": 0,
"open_issues_count": 3, "open_issues_count": 3,
"public_builds": true "public_builds": true,
"shared_with_groups": []
} }
] ]
} }
......
...@@ -82,7 +82,8 @@ Parameters: ...@@ -82,7 +82,8 @@ Parameters:
"forks_count": 0, "forks_count": 0,
"star_count": 0, "star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02", "runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true "public_builds": true,
"shared_with_groups": []
}, },
{ {
"id": 6, "id": 6,
...@@ -140,7 +141,8 @@ Parameters: ...@@ -140,7 +141,8 @@ Parameters:
"forks_count": 0, "forks_count": 0,
"star_count": 0, "star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02", "runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true "public_builds": true,
"shared_with_groups": []
} }
] ]
``` ```
...@@ -262,7 +264,20 @@ Parameters: ...@@ -262,7 +264,20 @@ Parameters:
"shared_runners_enabled": true, "shared_runners_enabled": true,
"forks_count": 0, "forks_count": 0,
"star_count": 0, "star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
"shared_with_groups": [
{
"group_id": 4,
"group_name": "Twitter",
"group_access_level": 30
},
{
"group_id": 3,
"group_name": "Gitlab Org",
"group_access_level": 10
}
]
} }
``` ```
...@@ -553,7 +568,9 @@ Example response: ...@@ -553,7 +568,9 @@ Example response:
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true, "shared_runners_enabled": true,
"forks_count": 0, "forks_count": 0,
"star_count": 1 "star_count": 1,
"public_builds": true,
"shared_with_groups": []
} }
``` ```
...@@ -616,7 +633,9 @@ Example response: ...@@ -616,7 +633,9 @@ Example response:
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true, "shared_runners_enabled": true,
"forks_count": 0, "forks_count": 0,
"star_count": 0 "star_count": 0,
"public_builds": true,
"shared_with_groups": []
} }
``` ```
...@@ -699,7 +718,9 @@ Example response: ...@@ -699,7 +718,9 @@ Example response:
"shared_runners_enabled": true, "shared_runners_enabled": true,
"forks_count": 0, "forks_count": 0,
"star_count": 0, "star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
"shared_with_groups": []
} }
``` ```
...@@ -782,7 +803,9 @@ Example response: ...@@ -782,7 +803,9 @@ Example response:
"shared_runners_enabled": true, "shared_runners_enabled": true,
"forks_count": 0, "forks_count": 0,
"star_count": 0, "star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
"shared_with_groups": []
} }
``` ```
......
...@@ -58,6 +58,14 @@ module API ...@@ -58,6 +58,14 @@ module API
expose :path, :path_with_namespace expose :path, :path_with_namespace
end end
class SharedGroup < Grape::Entity
expose :group_id
expose :group_name do |group_link, options|
group_link.group.name
end
expose :group_access, as: :group_access_level
end
class Project < Grape::Entity class Project < Grape::Entity
expose :id, :description, :default_branch, :tag_list expose :id, :description, :default_branch, :tag_list
expose :public?, as: :public expose :public?, as: :public
...@@ -77,6 +85,9 @@ module API ...@@ -77,6 +85,9 @@ module API
expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds expose :public_builds
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
end end
class ProjectMember < UserBasic class ProjectMember < UserBasic
...@@ -93,6 +104,7 @@ module API ...@@ -93,6 +104,7 @@ module API
class GroupDetail < Group class GroupDetail < Group
expose :projects, using: Entities::Project expose :projects, using: Entities::Project
expose :shared_projects, using: Entities::Project
end end
class GroupMember < UserBasic class GroupMember < UserBasic
......
...@@ -49,10 +49,25 @@ describe API::API, api: true do ...@@ -49,10 +49,25 @@ describe API::API, api: true do
describe "GET /groups/:id" do describe "GET /groups/:id" do
context "when authenticated as user" do context "when authenticated as user" do
it "should return one of user1's groups" do it "returns one of user1's groups" do
project = create(:project, namespace: group2, path: 'Foo')
create(:project_group_link, project: project, group: group1)
get api("/groups/#{group1.id}", user1) get api("/groups/#{group1.id}", user1)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
json_response['name'] == group1.name expect(json_response['id']).to eq(group1.id)
expect(json_response['name']).to eq(group1.name)
expect(json_response['path']).to eq(group1.path)
expect(json_response['description']).to eq(group1.description)
expect(json_response['visibility_level']).to eq(group1.visibility_level)
expect(json_response['avatar_url']).to eq(group1.avatar_url)
expect(json_response['web_url']).to eq(group1.web_url)
expect(json_response['projects']).to be_an Array
expect(json_response['projects'].length).to eq(2)
expect(json_response['shared_projects']).to be_an Array
expect(json_response['shared_projects'].length).to eq(1)
expect(json_response['shared_projects'][0]['id']).to eq(project.id)
end end
it "should not return a non existing group" do it "should not return a non existing group" do
......
...@@ -392,11 +392,47 @@ describe API::API, api: true do ...@@ -392,11 +392,47 @@ describe API::API, api: true do
before { project } before { project }
before { project_member } before { project_member }
it 'should return a project by id' do it 'returns a project by id' do
group = create(:group)
link = create(:project_group_link, project: project, group: group)
get api("/projects/#{project.id}", user) get api("/projects/#{project.id}", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['id']).to eq(project.id)
expect(json_response['description']).to eq(project.description)
expect(json_response['default_branch']).to eq(project.default_branch)
expect(json_response['tag_list']).to be_an Array
expect(json_response['public']).to be_falsey
expect(json_response['archived']).to be_falsey
expect(json_response['visibility_level']).to be_present
expect(json_response['ssh_url_to_repo']).to be_present
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['owner']).to be_a Hash
expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name) expect(json_response['name']).to eq(project.name)
expect(json_response['owner']['username']).to eq(user.username) expect(json_response['path']).to be_present
expect(json_response['issues_enabled']).to be_present
expect(json_response['merge_requests_enabled']).to be_present
expect(json_response['wiki_enabled']).to be_present
expect(json_response['builds_enabled']).to be_present
expect(json_response['snippets_enabled']).to be_present
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
expect(json_response['shared_runners_enabled']).to be_present
expect(json_response['creator_id']).to be_present
expect(json_response['namespace']).to be_present
expect(json_response['avatar_url']).to be_nil
expect(json_response['star_count']).to be_present
expect(json_response['forks_count']).to be_present
expect(json_response['public_builds']).to be_present
expect(json_response['shared_with_groups']).to be_an Array
expect(json_response['shared_with_groups'].length).to eq(1)
expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
end end
it 'should return a project by path name' do it 'should return a project by path name' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment