From c47d8ab69e108ef0cb30463fc2f02fbd1d03409b Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 19 Nov 2016 15:40:41 +0000 Subject: [PATCH 01/20] Removed leave buttons from settings dropdowns Updated specs --- .../layouts/nav/_group_settings.html.haml | 12 ++------- app/views/layouts/nav/_project.html.haml | 17 +++--------- .../members/_access_request_buttons.html.haml | 26 ++++++++++++------- ...ject-and-leave-group-should-be-buttons.yml | 5 ++++ .../last_owner_cannot_leave_group_spec.rb | 4 +-- .../members/member_leaves_group_spec.rb | 2 +- .../members/user_requests_access_spec.rb | 2 +- ..._member_cannot_leave_group_project_spec.rb | 2 +- ...r_cannot_request_access_to_project_spec.rb | 2 +- .../members/member_leaves_project_spec.rb | 2 +- .../owner_cannot_leave_project_spec.rb | 4 +-- 11 files changed, 36 insertions(+), 42 deletions(-) create mode 100644 changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index c0328fe8842..1579d8f1662 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,10 +1,8 @@ - if current_user - can_admin_group = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group) - - member = @group.members.find_by(user_id: current_user.id) - - can_leave = member && can?(current_user, :destroy_group_member, member) - - if can_admin_group || can_edit || can_leave + - if can_admin_group || can_edit .controls .dropdown.group-settings-dropdown %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} @@ -14,13 +12,7 @@ - if can_admin_group = nav_link(path: 'groups#projects') do = link_to 'Projects', projects_group_path(@group), title: 'Projects' - - if (can_edit || can_leave) && can_admin_group + - if can_edit && can_admin_group %li.divider - - if can_edit %li = link_to 'Edit Group', edit_group_path(@group) - - if can_leave - %li - = link_to polymorphic_path([:leave, @group, :members]), - data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do - Leave Group diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 99a58bbb676..cc24e51b268 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -6,23 +6,14 @@ = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - can_edit = can?(current_user, :admin_project, @project) - -# We don't use @project.team.find_member because it searches for group members too... - - member = @project.members.find_by(user_id: current_user.id) - - can_leave = member && can?(current_user, :destroy_project_member, member) = render 'layouts/nav/project_settings', can_edit: can_edit - - if can_edit || can_leave + - if can_edit %li.divider - - if can_edit - %li - = link_to edit_project_path(@project) do - Edit Project - - if can_leave - %li - = link_to polymorphic_path([:leave, @project, :members]), - data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do - Leave Project + %li + = link_to edit_project_path(@project) do + Edit Project .scrolling-tabs-container{ class: nav_control_class } .fade-left diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index eff914398bb..e166dfab710 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -1,10 +1,16 @@ -- if can?(current_user, :request_access, source) - - if requester = source.requesters.find_by(user_id: current_user.id) - = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), - method: :delete, - data: { confirm: remove_member_message(requester) }, - class: 'btn' - - else - = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), - method: :post, - class: 'btn' +- model_name = source.model_name.to_s.downcase + +- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) + = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: leave_confirmation_message(source) }, + class: 'btn' +- elsif requester = source.requesters.find_by(user_id: current_user.id) + = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: remove_member_message(requester) }, + class: 'btn' +- elsif source.request_access_enabled && can?(current_user, :request_access, source) + = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), + method: :post, + class: 'btn' diff --git a/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml new file mode 100644 index 00000000000..99dbe4a32a0 --- /dev/null +++ b/changelogs/unreleased/23305-leave-project-and-leave-group-should-be-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Moved Leave Project and Leave Group buttons to access_request_buttons from + the settings dropdown +merge_request: 7600 +author: diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb index 33bf6d3752f..be60b0489c7 100644 --- a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb +++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb @@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do visit group_path(group) end - scenario 'user does not see a "Leave Group" link' do - expect(page).not_to have_content 'Leave Group' + scenario 'user does not see a "Leave group" link' do + expect(page).not_to have_content 'Leave group' end end diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb index 3185ff924b9..ac4d94658ae 100644 --- a/spec/features/groups/members/member_leaves_group_spec.rb +++ b/spec/features/groups/members/member_leaves_group_spec.rb @@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do end scenario 'user leaves group' do - click_link 'Leave Group' + click_link 'Leave group' expect(current_path).to eq(dashboard_groups_path) expect(group.users.exists?(user.id)).to be_falsey diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb index d8c9c487996..e4b5ea91bd3 100644 --- a/spec/features/groups/members/user_requests_access_spec.rb +++ b/spec/features/groups/members/user_requests_access_spec.rb @@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do expect(page).to have_content 'Your request for access has been queued for review.' expect(page).to have_content 'Withdraw Access Request' - expect(page).not_to have_content 'Leave Group' + expect(page).not_to have_content 'Leave group' end scenario 'user does not see private projects' do diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb index 728c0e16361..b483ba4c54c 100644 --- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb +++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb @@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature: end scenario 'user does not see a "Leave project" link' do - expect(page).not_to have_content 'Leave Project' + expect(page).not_to have_content 'Leave project' end end diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb index 4973e0aee85..bdeeef57273 100644 --- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb +++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projects > Members > Group requester cannot request access to project', feature: true do +feature 'Projects > Members > Group requester cannot request access to project', feature: true, js: true do let(:user) { create(:user) } let(:owner) { create(:user) } let(:group) { create(:group, :public, :access_requestable) } diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index 79dec442818..5daa932e4e6 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do end scenario 'user leaves project' do - click_link 'Leave Project' + click_link 'Leave project' expect(current_path).to eq(dashboard_projects_path) expect(project.users.exists?(user.id)).to be_falsey diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb index 6e948b7a616..b26d55c5d5d 100644 --- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb +++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb @@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do visit namespace_project_path(project.namespace, project) end - scenario 'user does not see a "Leave Project" link' do - expect(page).not_to have_content 'Leave Project' + scenario 'user does not see a "Leave project" link' do + expect(page).not_to have_content 'Leave project' end end From fde754e2676e40dcf2600190983ef54030c5d5a5 Mon Sep 17 00:00:00 2001 From: Guyzmo Date: Sat, 26 Nov 2016 16:37:26 +0100 Subject: [PATCH 02/20] API: Endpoint to expose personal snippets as /snippets Adding the necessary API for the new /snippets Restful resource added with this commit. Added a new Grape class `Snippets`, as well as a `PersonalSnippet` entity. Issue: #20042 Merge-Request: !6373 Signed-off-by: Guyzmo --- app/finders/snippets_finder.rb | 5 +- app/helpers/gitlab_routing_helper.rb | 5 + app/policies/personal_snippet_policy.rb | 5 + .../unreleased/features-api-snippets.yml | 4 + doc/api/snippets.md | 232 ++++++++++++++++++ lib/api/api.rb | 1 + lib/api/entities.rb | 13 + lib/api/snippets.rb | 137 +++++++++++ lib/gitlab/url_builder.rb | 2 + spec/finders/snippets_finder_spec.rb | 59 +++-- spec/requests/api/snippets_spec.rb | 157 ++++++++++++ 11 files changed, 594 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/features-api-snippets.yml create mode 100644 doc/api/snippets.md create mode 100644 lib/api/snippets.rb create mode 100644 spec/requests/api/snippets_spec.rb diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 00ff1611039..0586a923a74 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -1,12 +1,15 @@ class SnippetsFinder def execute(current_user, params = {}) filter = params[:filter] + user = params.fetch(:user, current_user) case filter when :all then snippets(current_user).fresh + when :public then + Snippet.are_public.fresh when :by_user then - by_user(current_user, params[:user], params[:scope]) + by_user(current_user, user, params[:scope]) when :by_project by_project(current_user, params[:project]) end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index af9087d8326..99db73c9ee0 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -159,6 +159,11 @@ module GitlabRoutingHelper resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) end + # Snippets + def personal_snippet_url(snippet, *args) + snippet_url(snippet) + end + # Groups ## Members diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index 46c5aa1a5be..d3913986cd8 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy if @subject.author == @user can! :read_personal_snippet can! :update_personal_snippet + can! :destroy_personal_snippet can! :admin_personal_snippet end + unless @user.external? + can! :create_personal_snippet + end + if @subject.internal? && !@user.external? can! :read_personal_snippet end diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml new file mode 100644 index 00000000000..80c7bb75359 --- /dev/null +++ b/changelogs/unreleased/features-api-snippets.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Endpoint to expose personal snippets as /snippets' +merge_request: 6373 +author: Bernard Guyzmo Pratz diff --git a/doc/api/snippets.md b/doc/api/snippets.md new file mode 100644 index 00000000000..5a5dc162ffe --- /dev/null +++ b/doc/api/snippets.md @@ -0,0 +1,232 @@ +# Snippets + +> [Introduced][ce-6373] in GitLab 8.15. + +### Snippet visibility level + +Snippets in GitLab can be either private, internal, or public. +You can set it with the `visibility_level` field in the snippet. + +Constants for snippet visibility levels are: + +| Visibility | Visibility level | Description | +| ---------- | ---------------- | ----------- | +| Private | `0` | The snippet is visible only to the snippet creator | +| Internal | `10` | The snippet is visible for any logged in user | +| Public | `20` | The snippet can be accessed without any authentication | + +## List snippets + +Get a list of current user's snippets. + +``` +GET /snippets +``` + +## Single snippet + +Get a single snippet. + +``` +GET /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | + +``` bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1 +``` + +Example response: + +``` json +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Create new snippet + +Creates a new snippet. The user must have permission to create new snippets. + +``` +POST /snippets +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `title` | String | yes | The title of a snippet | +| `file_name` | String | yes | The name of a snippet file | +| `content` | String | yes | The content of a snippet | +| `visibility_level` | Integer | yes | The snippet's visibility | + + +``` bash +curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets +``` + +Example response: + +``` json +{ + "id": 1, + "title": "This is a snippet", + "file_name": "test.txt", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Update snippet + +Updates an existing snippet. The user must have permission to change an existing snippet. + +``` +PUT /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | +| `title` | String | no | The title of a snippet | +| `file_name` | String | no | The name of a snippet file | +| `content` | String | no | The content of a snippet | +| `visibility_level` | Integer | no | The snippet's visibility | + + +``` bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1 +``` + +Example response: + +``` json +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} +``` + +## Delete snippet + +Deletes an existing snippet. + +``` +DELETE /snippets/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | + + +``` +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1" +``` + +upon successful delete a `204 No content` HTTP code shall be expected, with no data, +but if the snippet is non-existent, a `404 Not Found` will be returned. + +## Explore all public snippets + +``` +GET /snippets/public +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `per_page` | Integer | no | number of snippets to return per page | +| `page` | Integer | no | the page to retrieve | + +``` bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1 +``` + +Example response: + +``` json +[ + { + "author": { + "avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon", + "id": 12, + "name": "Libby Rolfson", + "state": "active", + "username": "elton_wehner", + "web_url": "http://localhost:3000/elton_wehner" + }, + "created_at": "2016-11-25T16:53:34.504Z", + "file_name": "oconnerrice.rb", + "id": 49, + "raw_url": "http://localhost:3000/snippets/49/raw", + "title": "Ratione cupiditate et laborum temporibus.", + "updated_at": "2016-11-25T16:53:34.504Z", + "web_url": "http://localhost:3000/snippets/49" + }, + { + "author": { + "avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon", + "id": 16, + "name": "Llewellyn Flatley", + "state": "active", + "username": "adaline", + "web_url": "http://localhost:3000/adaline" + }, + "created_at": "2016-11-25T16:53:34.479Z", + "file_name": "muellershields.rb", + "id": 48, + "raw_url": "http://localhost:3000/snippets/48/raw", + "title": "Minus similique nesciunt vel fugiat qui ullam sunt.", + "updated_at": "2016-11-25T16:53:34.479Z", + "web_url": "http://localhost:3000/snippets/48" + } +] +``` + diff --git a/lib/api/api.rb b/lib/api/api.rb index 67109ceeef9..cec2702e44d 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -64,6 +64,7 @@ module API mount ::API::Session mount ::API::Settings mount ::API::SidekiqMetrics + mount ::API::Snippets mount ::API::Subscriptions mount ::API::SystemHooks mount ::API::Tags diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d5dfb8d00be..dc6d085925d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -200,6 +200,19 @@ module API end end + class PersonalSnippet < Grape::Entity + expose :id, :title, :file_name + expose :author, using: Entities::UserBasic + expose :updated_at, :created_at + + expose :web_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + end + expose :raw_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + "/raw" + end + end + class ProjectEntity < Grape::Entity expose :id, :iid expose(:project_id) { |entity| entity.project.id } diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb new file mode 100644 index 00000000000..e096e636806 --- /dev/null +++ b/lib/api/snippets.rb @@ -0,0 +1,137 @@ +module API + # Snippets API + class Snippets < Grape::API + include PaginationParams + + before { authenticate! } + + resource :snippets do + helpers do + def snippets_for_current_user + SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user) + end + + def public_snippets + SnippetsFinder.new.execute(current_user, filter: :public) + end + end + + desc 'Get a snippets list for authenticated user' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + use :pagination + end + get do + present paginate(snippets_for_current_user), with: Entities::PersonalSnippet + end + + desc 'List all public snippets current_user has access to' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + use :pagination + end + get 'public' do + present paginate(public_snippets), with: Entities::PersonalSnippet + end + + desc 'Get a single snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ':id' do + snippet = snippets_for_current_user.find(params[:id]) + present snippet, with: Entities::PersonalSnippet + end + + desc 'Create new snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :title, type: String, desc: 'The title of a snippet' + requires :file_name, type: String, desc: 'The name of a snippet file' + requires :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + default: Gitlab::VisibilityLevel::INTERNAL, + desc: 'The visibility level of the snippet' + end + post do + attrs = declared_params(include_missing: false) + snippet = CreateSnippetService.new(nil, current_user, attrs).execute + + if snippet.persisted? + present snippet, with: Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Update an existing snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + optional :title, type: String, desc: 'The title of a snippet' + optional :file_name, type: String, desc: 'The name of a snippet file' + optional :content, type: String, desc: 'The content of a snippet' + optional :visibility_level, type: Integer, + values: Gitlab::VisibilityLevel.values, + desc: 'The visibility level of the snippet' + at_least_one_of :title, :file_name, :content, :visibility_level + end + put ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet + + attrs = declared_params(include_missing: false) + + UpdateSnippetService.new(nil, current_user, snippet, attrs).execute + if snippet.persisted? + present snippet, with: Entities::PersonalSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Remove snippet' do + detail 'This feature was introduced in GitLab 8.15.' + success Entities::PersonalSnippet + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + delete ':id' do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet + snippet.destroy + no_content! + end + + desc 'Get a raw snippet' do + detail 'This feature was introduced in GitLab 8.15.' + end + params do + requires :id, type: Integer, desc: 'The ID of a snippet' + end + get ":id/raw" do + snippet = snippets_for_current_user.find_by(id: params.delete(:id)) + return not_found!('Snippet') unless snippet + + env['api.format'] = :txt + content_type 'text/plain' + present snippet.content + end + end + end +end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 99d0c28e749..ccb456bcc94 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -24,6 +24,8 @@ module Gitlab wiki_page_url when ProjectSnippet project_snippet_url(object) + when Snippet + personal_snippet_url(object) else raise NotImplementedError.new("No URL builder defined for #{object.class}") end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 28bdc18e840..4427443208a 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -9,65 +9,74 @@ describe SnippetsFinder do let(:project2) { create(:empty_project, :private, group: group) } context ':all filter' do - before do - @snippet1 = create(:personal_snippet, :private) - @snippet2 = create(:personal_snippet, :internal) - @snippet3 = create(:personal_snippet, :public) - end + let!(:snippet1) { create(:personal_snippet, :private) } + let!(:snippet2) { create(:personal_snippet, :internal) } + let!(:snippet3) { create(:personal_snippet, :public) } it "returns all private and internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :all) - expect(snippets).to include(@snippet2, @snippet3) - expect(snippets).not_to include(@snippet1) + expect(snippets).to include(snippet2, snippet3) + expect(snippets).not_to include(snippet1) end it "returns all public snippets" do snippets = SnippetsFinder.new.execute(nil, filter: :all) - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) + end + end + + context ':public filter' do + let!(:snippet1) { create(:personal_snippet, :private) } + let!(:snippet2) { create(:personal_snippet, :internal) } + let!(:snippet3) { create(:personal_snippet, :public) } + + it "returns public public snippets" do + snippets = SnippetsFinder.new.execute(nil, filter: :public) + + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end end context ':by_user filter' do - before do - @snippet1 = create(:personal_snippet, :private, author: user) - @snippet2 = create(:personal_snippet, :internal, author: user) - @snippet3 = create(:personal_snippet, :public, author: user) - end + let!(:snippet1) { create(:personal_snippet, :private, author: user) } + let!(:snippet2) { create(:personal_snippet, :internal, author: user) } + let!(:snippet3) { create(:personal_snippet, :public, author: user) } it "returns all public and internal snippets" do snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) - expect(snippets).to include(@snippet2, @snippet3) - expect(snippets).not_to include(@snippet1) + expect(snippets).to include(snippet2, snippet3) + expect(snippets).not_to include(snippet1) end it "returns internal snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") - expect(snippets).to include(@snippet2) - expect(snippets).not_to include(@snippet1, @snippet3) + expect(snippets).to include(snippet2) + expect(snippets).not_to include(snippet1, snippet3) end it "returns private snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") - expect(snippets).to include(@snippet1) - expect(snippets).not_to include(@snippet2, @snippet3) + expect(snippets).to include(snippet1) + expect(snippets).not_to include(snippet2, snippet3) end it "returns public snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet1, snippet2) end it "returns all snippets" do snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) - expect(snippets).to include(@snippet1, @snippet2, @snippet3) + expect(snippets).to include(snippet1, snippet2, snippet3) end it "returns only public snippets if unauthenticated user" do snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet2, @snippet1) + expect(snippets).to include(snippet3) + expect(snippets).not_to include(snippet2, snippet1) end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb new file mode 100644 index 00000000000..f6fb6ea5506 --- /dev/null +++ b/spec/requests/api/snippets_spec.rb @@ -0,0 +1,157 @@ +require 'rails_helper' + +describe API::Snippets, api: true do + include ApiHelpers + let!(:user) { create(:user) } + + describe 'GET /snippets/' do + it 'returns snippets available' do + public_snippet = create(:personal_snippet, :public, author: user) + private_snippet = create(:personal_snippet, :private, author: user) + internal_snippet = create(:personal_snippet, :internal, author: user) + + get api("/snippets/", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + internal_snippet.id, + private_snippet.id) + expect(json_response.last).to have_key('web_url') + expect(json_response.last).to have_key('raw_url') + end + + it 'hides private snippets from regular user' do + create(:personal_snippet, :private) + + get api("/snippets/", user) + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + end + + describe 'GET /snippets/public' do + let!(:other_user) { create(:user) } + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + let!(:private_snippet) { create(:personal_snippet, :private, author: user) } + let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } + let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } + let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } + let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } + + it 'returns all snippets with public visibility from all users' do + get api("/snippets/public", user) + + expect(response).to have_http_status(200) + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + public_snippet_other.id) + expect(json_response.map{ |snippet| snippet['web_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}", + "http://localhost/snippets/#{public_snippet_other.id}") + expect(json_response.map{ |snippet| snippet['raw_url']} ).to include( + "http://localhost/snippets/#{public_snippet.id}/raw", + "http://localhost/snippets/#{public_snippet_other.id}/raw") + end + end + + describe 'GET /snippets/:id/raw' do + let(:snippet) { create(:personal_snippet, author: user) } + + it 'returns raw text' do + get api("/snippets/#{snippet.id}/raw", user) + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to eq(snippet.content) + end + + it 'returns 404 for invalid snippet id' do + delete api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + + describe 'POST /snippets/' do + let(:params) do + { + title: 'Test Title', + file_name: 'test.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PUBLIC + } + end + + it 'creates a new snippet' do + expect do + post api("/snippets/", user), params + end.to change { PersonalSnippet.count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq(params[:title]) + expect(json_response['file_name']).to eq(params[:file_name]) + end + + it 'returns 400 for missing parameters' do + params.delete(:title) + + post api("/snippets/", user), params + + expect(response).to have_http_status(400) + end + end + + describe 'PUT /snippets/:id' do + let(:other_user) { create(:user) } + let(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'updates snippet' do + new_content = 'New content' + + put api("/snippets/#{public_snippet.id}", user), content: new_content + + expect(response).to have_http_status(200) + public_snippet.reload + expect(public_snippet.content).to eq(new_content) + end + + it 'returns 404 for invalid snippet id' do + put api("/snippets/1234", user), title: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it "returns 404 for another user's snippet" do + put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it 'returns 400 for missing parameters' do + put api("/snippets/1234", user) + + expect(response).to have_http_status(400) + end + end + + describe 'DELETE /snippets/:id' do + let!(:public_snippet) { create(:personal_snippet, :public, author: user) } + it 'deletes snippet' do + expect do + delete api("/snippets/#{public_snippet.id}", user) + + expect(response).to have_http_status(204) + end.to change { PersonalSnippet.count }.by(-1) + end + + it 'returns 404 for invalid snippet id' do + delete api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end +end From 3ce2ba1afb1dad707e1e3ac2bb5ff856d06f621e Mon Sep 17 00:00:00 2001 From: basyura Date: Fri, 25 Nov 2016 23:09:50 +0900 Subject: [PATCH 03/20] fix display hook error message --- app/assets/javascripts/merge_request_widget.js.es6 | 2 +- changelogs/unreleased/issue_24020.yml | 4 ++++ spec/javascripts/merge_request_widget_spec.js | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/issue_24020.yml diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index a55fe9df0b3..d9495e50388 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -101,7 +101,7 @@ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; return window.location.href = window.location.pathname + urlSuffix; } else if (data.merge_error) { - return this.$widgetBody.html("

" + data.merge_error + "

"); + return _this.$widgetBody.html("

" + data.merge_error + "

"); } else { callback = function() { return merge_request_widget.mergeInProgress(deleteSourceBranch); diff --git a/changelogs/unreleased/issue_24020.yml b/changelogs/unreleased/issue_24020.yml new file mode 100644 index 00000000000..87310b75296 --- /dev/null +++ b/changelogs/unreleased/issue_24020.yml @@ -0,0 +1,4 @@ +--- +title: "fix display hook error message" +merge_request: 7775 +author: basyura diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 62890f1ca96..6f91529db00 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -106,6 +106,18 @@ }); }); + describe('mergeInProgress', function() { + it('should display error with h4 tag', function() { + spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) { + expect(html).toBe('

Sorry, something went wrong.

'); + }); + spyOn($, 'ajax').and.callFake(function(e) { + e.success({ merge_error: 'Sorry, something went wrong.' }); + }); + this.class.mergeInProgress(null); + }); + }); + return describe('getCIStatus', function() { beforeEach(function() { this.ciStatusData = { From 9d9fe3c82efa8c0ec75863b5b533af6fdbe48f89 Mon Sep 17 00:00:00 2001 From: Ryan Harris Date: Tue, 6 Dec 2016 12:55:24 -0500 Subject: [PATCH 04/20] Use default btn styling for Housekeeping button on projects settings page --- app/views/projects/edit.html.haml | 2 +- .../unreleased/25324-change-housekeeping-btn-to-default.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 3a5af2723c6..f8d856fe152 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -145,7 +145,7 @@ such as compressing file revisions and removing unreachable objects. .col-lg-9 = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), - method: :post, class: "btn btn-save" + method: :post, class: "btn btn-default" %hr .row.prepend-top-default .col-lg-3 diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml new file mode 100644 index 00000000000..0770f9752a0 --- /dev/null +++ b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml @@ -0,0 +1,4 @@ +--- +title: Changed Housekeeping button on project settings page to default styling +merge_request: +author: Ryan Harris From 19fb84e3a897f5b251fb3f437fe365c6ec342c34 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 7 Dec 2016 15:27:14 +0000 Subject: [PATCH 05/20] Updated members dropdowns This ports some code over from EE to reduce conflicts --- app/assets/javascripts/gl_dropdown.js | 5 ++ app/assets/javascripts/members.js.es6 | 71 ++++++++++++++++--- .../stylesheets/framework/dropdowns.scss | 5 ++ app/assets/stylesheets/pages/members.scss | 4 ++ app/views/groups/group_members/update.js.haml | 1 + .../projects/project_members/update.js.haml | 1 + app/views/shared/members/_member.html.haml | 20 +++++- changelogs/unreleased/members-dropdowns.yml | 4 ++ features/steps/group/members.rb | 7 +- features/steps/project/team_management.rb | 6 +- 10 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 changelogs/unreleased/members-dropdowns.yml diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 969778dded7..9a91018a8e4 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -650,6 +650,11 @@ } else if(value) { field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); } + + if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { + return; + } + if (el.hasClass(ACTIVE_CLASS)) { el.removeClass(ACTIVE_CLASS); if (field && field.length) { diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 index 895bc10784f..64826894965 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js.es6 @@ -1,38 +1,87 @@ -/* eslint-disable */ -((w) => { - w.gl = w.gl || {}; +/* eslint-disable class-methods-use-this */ +(() => { + window.gl = window.gl || {}; class Members { constructor() { this.addListeners(); + this.initGLDropdown(); } addListeners() { $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); - $('.js-member-update-control').off('change').on('change', this.formSubmit); - $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); + $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); + $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); } + initGLDropdown() { + $('.js-member-permissions-dropdown').each((i, btn) => { + const $btn = $(btn); + + $btn.glDropdown({ + selectable: true, + isSelectable(selected, $el) { + return !$el.hasClass('is-active'); + }, + fieldName: $btn.data('field-name'), + id(selected, $el) { + return $el.data('id'); + }, + toggleLabel(selected, $el) { + return $el.text(); + }, + clicked: (selected, $el) => { + const $link = $($el); + const { $toggle, $dateInput } = this.getMemberListItems($link); + + $toggle.attr('disabled', true); + $dateInput.attr('disabled', true); + + $btn.closest('form').trigger('submit.rails'); + }, + }); + }); + } + removeRow(e) { const $target = $(e.target); if ($target.hasClass('btn-remove')) { $target.closest('.member') - .fadeOut(function () { + .fadeOut(function fadeOutMemberRow() { $(this).remove(); }); } } - formSubmit() { - $(this).closest('form').trigger("submit.rails").end().disable(); + formSubmit(e) { + const $this = $(e.currentTarget); + const { $toggle, $dateInput } = this.getMemberListItems($this); + + $this.closest('form').trigger('submit.rails'); + + $toggle.attr('disabled', true); + $dateInput.attr('disabled', true); } - formSuccess() { - $(this).find('.js-member-update-control').enable(); + formSuccess(e) { + const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); + + $toggle.removeAttr('disabled'); + $dateInput.removeAttr('disabled'); + } + + getMemberListItems($el) { + const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); + + return { + $memberListItem, + $toggle: $memberListItem.find('.dropdown-menu-toggle'), + $dateInput: $memberListItem.find('.js-access-expiration-date'), + }; } } gl.Members = Members; -})(window); +})(); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 33de652c06f..d5914b900e2 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -42,6 +42,11 @@ border-radius: $border-radius-base; white-space: nowrap; + &[disabled] { + background-color: $input-bg-disabled; + cursor: not-allowed; + } + &.no-outline { outline: 0; } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 756efa9c7fa..5f3bbb40ba0 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -54,6 +54,10 @@ @media (min-width: $screen-sm-min) { width: 50%; } + + .dropdown-menu-toggle { + width: 100%; + } } .member-access-text { diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index de8f53b6b52..9d05bff6c4e 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}")); diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 91927181efb..d15f4310ff5 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}")); diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 432047a1c4e..ac3e4d9bac6 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -48,9 +48,25 @@ - if show_controls - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member + = f.hidden_field :access_level + .member-form-control.dropdown.append-right-5 + %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", + disabled: !can_admin_member, + data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } + %span.dropdown-toggle-text + = member.human_access + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + = dropdown_title("Change permissions") + .dropdown-content + %ul + - Gitlab::Access.options.each do |role, role_id| + %li + = link_to role, "javascript:void(0)", + class: ("is-active" if member.access_level == role_id), + data: { id: role_id, el_id: dom_id(member) } .prepend-left-5.clearable-input.member-form-control - = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member + = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) } %i.clear-icon.js-clear-input - else %span.member-access-text= member.human_access diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml new file mode 100644 index 00000000000..b15403d6d62 --- /dev/null +++ b/changelogs/unreleased/members-dropdowns.yml @@ -0,0 +1,4 @@ +--- +title: Updated members dropdowns +merge_request: +author: diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb index cefc55d07ab..adaf375453c 100644 --- a/features/steps/group/members.rb +++ b/features/steps/group/members.rb @@ -117,7 +117,12 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps member = mary_jane_member page.within "#group_member_#{member.id}" do - select 'Developer', from: "member_access_level_#{member.id}" + click_button member.human_access + + page.within '.dropdown-menu' do + click_link 'Developer' + end + wait_for_ajax end end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index b21d0849ad1..99b6397ba74 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -65,7 +65,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps user = User.find_by(name: 'Dmitriy') project_member = project.project_members.find_by(user_id: user.id) page.within "#project_member_#{project_member.id}" do - select "Reporter", from: "member_access_level_#{project_member.id}" + click_button project_member.human_access + + page.within '.dropdown-menu' do + click_link 'Reporter' + end end end From fe447b43ac8a965b533c056f823d392f0c9db5ac Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 8 Dec 2016 11:53:21 +0100 Subject: [PATCH 06/20] Use gitlab-workhose 1.1.1 Confines API rate limiting feature to builds/register.json CI requests only. --- GITLAB_WORKHORSE_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 9084fa2f716..524cb55242b 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -1.1.0 +1.1.1 From d74ad9263048549e4d90e6a22313768109eaf2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 8 Dec 2016 18:29:25 +0100 Subject: [PATCH 07/20] Use a single query in Projects::ProjectMembersController to fetch members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../projects/project_members_controller.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 3fb8bba3cd0..53308948f62 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -35,13 +35,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - member_ids = @project_members.pluck(:id) + wheres = ["id IN (#{@project_members.select(:id).to_sql})"] + wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members - if group_members - member_ids += group_members.pluck(:id) - end - - @project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page]) + @project_members = Member. + where(wheres.join(' OR ')). + order(access_level: :desc).page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) From 4a0ca85834b6cf7decb58857986e535ba4e37c53 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Tue, 6 Dec 2016 18:56:59 -0200 Subject: [PATCH 08/20] Allow branch names with dots on API endpoint --- changelogs/unreleased/issue_25030.yml | 4 ++++ lib/api/branches.rb | 16 +++++++------- spec/requests/api/branches_spec.rb | 31 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/issue_25030.yml diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml new file mode 100644 index 00000000000..e18b8d6a79b --- /dev/null +++ b/changelogs/unreleased/issue_25030.yml @@ -0,0 +1,4 @@ +--- +title: Allow branch names with dots on API endpoint +merge_request: +author: diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 73aed624ea7..0950c3d2e88 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -23,9 +23,9 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - get ':id/repository/branches/:branch' do + get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do branch = user_project.repository.find_branch(params[:branch]) not_found!("Branch") unless branch @@ -39,11 +39,11 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' end - put ':id/repository/branches/:branch/protect' do + put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -76,9 +76,9 @@ module API success Entities::RepoBranch end params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - put ':id/repository/branches/:branch/unprotect' do + put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -112,9 +112,9 @@ module API desc 'Delete a branch' params do - requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + requires :branch, type: String, desc: 'The name of the branch' end - delete ":id/repository/branches/:branch" do + delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 55b8c8c0c69..2878e0cb59b 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -11,6 +11,7 @@ describe API::Branches, api: true do let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } describe "GET /projects/:id/repository/branches" do it "returns an array of project branches" do @@ -37,6 +38,13 @@ describe API::Branches, api: true do expect(json_response['developers_can_merge']).to eq(false) end + it "returns the branch information for a single branch with dots in the name" do + get api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + end + context 'on a merged branch' do it "returns the branch information for a single branch" do get api("/projects/#{project.id}/repository/branches/merge-test", user) @@ -71,6 +79,14 @@ describe API::Branches, api: true do expect(json_response['developers_can_merge']).to eq(false) end + it "protects a single branch with dots in the name" do + put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['protected']).to eq(true) + end + it 'protects a single branch and developers can push' do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true @@ -220,6 +236,14 @@ describe API::Branches, api: true do expect(json_response['protected']).to eq(false) end + it "update branches with dots in branch name" do + put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq("with.1.2.3") + expect(json_response['protected']).to eq(false) + end + it "returns success when unprotect branch" do put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) expect(response).to have_http_status(404) @@ -292,6 +316,13 @@ describe API::Branches, api: true do expect(json_response['branch_name']).to eq(branch_name) end + it "removes a branch with dots in the branch name" do + delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['branch_name']).to eq("with.1.2.3") + end + it 'returns 404 if branch not exists' do delete api("/projects/#{project.id}/repository/branches/foobar", user) expect(response).to have_http_status(404) From 1637ce4e6a64c154eca93444f6a62664f4e78e7a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 9 Dec 2016 10:06:02 +0000 Subject: [PATCH 09/20] Updated JS based on review Fixed group links dropdown to match --- app/assets/javascripts/members.js.es6 | 22 +++++++------------ app/views/projects/group_links/update.js.haml | 1 + app/views/shared/members/_group.html.haml | 21 ++++++++++++++++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 index 64826894965..e3f367a11eb 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js.es6 @@ -31,14 +31,8 @@ toggleLabel(selected, $el) { return $el.text(); }, - clicked: (selected, $el) => { - const $link = $($el); - const { $toggle, $dateInput } = this.getMemberListItems($link); - - $toggle.attr('disabled', true); - $dateInput.attr('disabled', true); - - $btn.closest('form').trigger('submit.rails'); + clicked: (selected, $link) => { + this.formSubmit(null, $link); }, }); }); @@ -55,21 +49,21 @@ } } - formSubmit(e) { - const $this = $(e.currentTarget); + formSubmit(e, $el = null) { + const $this = e ? $(e.currentTarget) : $el; const { $toggle, $dateInput } = this.getMemberListItems($this); $this.closest('form').trigger('submit.rails'); - $toggle.attr('disabled', true); - $dateInput.attr('disabled', true); + $toggle.disable(); + $dateInput.disable(); } formSuccess(e) { const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); - $toggle.removeAttr('disabled'); - $dateInput.removeAttr('disabled'); + $toggle.enable(); + $dateInput.enable(); } getMemberListItems($el) { diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml index af9a5b19060..55520fda494 100644 --- a/app/views/projects/group_links/update.js.haml +++ b/app/views/projects/group_links/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}'); $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}")); diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 1c0346bbc78..8928de9097b 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -1,7 +1,8 @@ - group_link = local_assigns[:group_link] - group = group_link.group - can_admin_member = can?(current_user, :admin_project_member, @project) -%li.member.group_member{ id: "group_member_#{group_link.id}" } +- dom_id = "group_member_#{group_link.id}" +%li.member.group_member{ id: dom_id } %span{ class: "list-item-name" } = image_tag group_icon(group), class: "avatar s40", alt: '' %strong @@ -14,7 +15,23 @@ Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .controls.member-controls = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do - = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member + = hidden_field_tag "group_link[group_access]", group_link.group_access + .member-form-control.dropdown.append-right-5 + %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", + disabled: !can_admin_member, + data: { toggle: "dropdown", field_name: "group_link[group_access]" } } + %span.dropdown-toggle-text + = group_link.human_access + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + = dropdown_title("Change permissions") + .dropdown-content + %ul + - Gitlab::Access.options.each do |role, role_id| + %li + = link_to role, "javascript:void(0)", + class: ("is-active" if group_link.group_access == role_id), + data: { id: role_id, el_id: dom_id } .prepend-left-5.clearable-input.member-form-control = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member %i.clear-icon.js-clear-input From 4f264c793046307f67b2b3a365cb2a41f1fc0eda Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Fri, 9 Dec 2016 16:46:03 +0100 Subject: [PATCH 10/20] Updates the font weight of button styles because of the change to system fonts --- app/assets/stylesheets/framework/buttons.scss | 2 +- changelogs/unreleased/update-button-font-weight.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/update-button-font-weight.yml diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 8da3da2ad08..1c7b2f4df7c 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,7 +1,7 @@ @mixin btn-default { border-radius: 3px; font-size: $gl-font-size; - font-weight: 500; + font-weight: 400; padding: $gl-vert-padding $gl-btn-padding; &:focus, diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml new file mode 100644 index 00000000000..ddb3c1c8da4 --- /dev/null +++ b/changelogs/unreleased/update-button-font-weight.yml @@ -0,0 +1,4 @@ +--- +title: Updates the font weight of button styles because of the change to system fonts +merge_request: +author: From 41dd01b1276d11ceec6d63e2ead52ddaf1852041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 9 Dec 2016 17:16:04 +0100 Subject: [PATCH 11/20] Stop replacing `$your_email` with the user's email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `$your_email` was removed from the SSH doc. Signed-off-by: Rémy Coutable --- app/views/help/show.html.haml | 2 +- spec/features/help_pages_spec.rb | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index be257b51b9e..f6ebd76af9d 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,3 @@ - page_title @path.split("/").reverse.map(&:humanize) .documentation.wiki - = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") + = markdown @markdown diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 4319d6db0d2..40a1fced8d8 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -1,16 +1,6 @@ require 'spec_helper' describe 'Help Pages', feature: true do - describe 'Show SSH page' do - before do - login_as :user - end - it 'replaces the variable $your_email with the email of the user' do - visit help_page_path('ssh/README') - expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"") - end - end - describe 'Get the main help page' do shared_examples_for 'help page' do |prefix: ''| it 'prefixes links correctly' do From fcf332a4fa544d4a5eba200878a7b9298e5b6f6b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 9 Dec 2016 11:35:15 +0000 Subject: [PATCH 12/20] Group links spec update --- features/steps/project/team_management.rb | 2 +- spec/features/projects/members/group_links_spec.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 99b6397ba74..22d971fadfb 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -148,7 +148,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I should see "Opensource" group user listing' do page.within '.project-members-groups' do expect(page).to have_content('OpenSource') - expect(find('select').value).to eq('40') + expect(first('.group_member')).to have_content('Master') end end end diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb index cc2f695211c..94995f7cf95 100644 --- a/spec/features/projects/members/group_links_spec.rb +++ b/spec/features/projects/members/group_links_spec.rb @@ -16,12 +16,17 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t end it 'updates group access level' do - select 'Guest', from: "member_access_level_#{group.id}" + click_button @group_link.human_access + + page.within '.dropdown-menu' do + click_link 'Guest' + end + wait_for_ajax visit namespace_project_project_members_path(project.namespace, project) - expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest') + expect(first('.group_member')).to have_content('Guest') end it 'updates expiry date' do From e01e9f8b01e530ea5aa43180abbbb6490684eb8c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 9 Dec 2016 15:28:41 -0200 Subject: [PATCH 13/20] [ci skip] Update "Installation from source" guide for 8.15.0 --- doc/install/installation.md | 4 ++-- doc/update/8.14-to-8.15.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 9c6a9656557..2740b2982b9 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -271,9 +271,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab -**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 3f58493fa63..5556dae2551 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v4.0.0 +sudo -u git -H git checkout v4.0.3 ``` ### 6. Update gitlab-workhorse From 69ffa81424e16a337b4c39d2cb9c0dff8b92f71b Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 23:30:54 -0600 Subject: [PATCH 14/20] fix alignment for notification settings ajax response --- app/controllers/notification_settings_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index 8ec4bb1233f..160259626d6 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -37,7 +37,11 @@ class NotificationSettingsController < ApplicationController def render_response render json: { - html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting), + html: view_to_html_string( + "shared/notifications/_button", + notification_setting: @notification_setting, + left_align: @notification_setting.source.nil? + ), saved: @saved } end From 097c283ac186bccb647143fd022bfe6a320379b0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 10 Dec 2016 00:21:47 -0600 Subject: [PATCH 15/20] remove left_align setting from notification setting dropdown in favor of a pure css solution --- app/assets/stylesheets/pages/notifications.scss | 4 ++++ app/assets/stylesheets/pages/projects.scss | 4 ++++ app/controllers/notification_settings_controller.rb | 6 +----- app/views/profiles/notifications/show.html.haml | 2 +- app/views/shared/notifications/_button.html.haml | 3 +-- .../shared/notifications/_notification_dropdown.html.haml | 3 +-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index 94fbbef3c77..7d61390a439 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -1,5 +1,9 @@ .notification-list-item { line-height: 34px; + + .dropdown-menu { + @extend .dropdown-menu-align-right; + } } .notification { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 72b6685d940..6e0f6b1cd81 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -188,6 +188,10 @@ margin-left: 10px; } + .notification-dropdown .dropdown-menu { + @extend .dropdown-menu-align-right; + } + .download-button { @media (max-width: $screen-md-max) { margin-left: 0; diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index 160259626d6..8ec4bb1233f 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -37,11 +37,7 @@ class NotificationSettingsController < ApplicationController def render_response render json: { - html: view_to_html_string( - "shared/notifications/_button", - notification_setting: @notification_setting, - left_align: @notification_setting.source.nil? - ), + html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting), saved: @saved } end diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 844fce59704..d79a1a9f368 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -30,7 +30,7 @@ %br .clearfix .form-group.pull-left.global-notification-setting - = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true + = render 'shared/notifications/button', notification_setting: @global_notification_setting .clearfix diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 1f7df0bcd19..fbad0d05de3 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,4 +1,3 @@ -- left_align = local_assigns[:left_align] - if notification_setting .dropdown.notification-dropdown = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| @@ -19,7 +18,7 @@ = notification_title(notification_setting.level) = icon("caret-down") - = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align + = render "shared/notifications/notification_dropdown", notification_setting: notification_setting = content_for :scripts_body do = render "shared/notifications/custom_notifications", notification_setting: notification_setting diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml index d3258ee64cb..85ad74f9a39 100644 --- a/app/views/shared/notifications/_notification_dropdown.html.haml +++ b/app/views/shared/notifications/_notification_dropdown.html.haml @@ -1,5 +1,4 @@ -- left_align = local_assigns[:left_align] -%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] } +%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] } - NotificationSetting.levels.each_key do |level| - next if level == "custom" - next if level == "global" && notification_setting.source.nil? From e71ed01bfbaef088e8f7927b01d5c470b4cfc5f2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 11 Dec 2016 10:18:44 +0100 Subject: [PATCH 16/20] Change docs title to represent the edition --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 66c8c26e4f0..eba1e9845b1 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,4 +1,4 @@ -# Documentation +# GitLab Community Edition documentation ## User documentation From d6a1a3e6bee6417d02a2095bce479f494460ce17 Mon Sep 17 00:00:00 2001 From: Ismail S Date: Sun, 11 Dec 2016 14:14:59 +0000 Subject: [PATCH 17/20] Fix typo --- doc/ci/review_apps/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index a66165dc973..c679ea4e298 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an also dynamically URL. The details of the Review Apps implementation depend widely on your real -technology stack and on your deployment process. The simplest case it to +technology stack and on your deployment process. The simplest case is to deploy a simple static HTML website, but it will not be that straightforward when your app is using a database for example. To make a branch be deployed on a temporary instance and booting up this instance with all required software From 36c24de5da7feb6392e8250e2d9e11e602d3e527 Mon Sep 17 00:00:00 2001 From: Georg Hartmann Date: Sun, 11 Dec 2016 19:36:01 +0000 Subject: [PATCH 18/20] Fix typo in curl example request --- doc/api/merge_requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 81df55ab4ab..662cc9da733 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id | `merge_request_id` | integer | yes | The ID of a project's merge request | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85 ``` ## Accept MR From 3c36d9dc9b3c2db45df6dce19357e9c4bdde366f Mon Sep 17 00:00:00 2001 From: jnoortheen Date: Tue, 6 Dec 2016 22:32:30 +0530 Subject: [PATCH 19/20] fix: removed signed_out notification test: replaced signed_out message check with check for sign_in button fixes #25294 --- app/controllers/sessions_controller.rb | 6 ++++++ changelogs/unreleased/25294-remove-signed-out-msg.yml | 4 ++++ spec/support/login_helpers.rb | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25294-remove-signed-out-msg.yml diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 38e7c6f4a48..8c698695202 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController end end + def destroy + super + # hide the signed_out notice + flash[:notice] = nil + end + private # Handle an "initial setup" state, where there's only one user, it's an admin, diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml new file mode 100644 index 00000000000..567294fe5f7 --- /dev/null +++ b/changelogs/unreleased/25294-remove-signed-out-msg.yml @@ -0,0 +1,4 @@ +--- +title: 'fix: removed signed_out notification' +merge_request: 7958 +author: jnoortheen diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index c0b3e83244d..ad1eed5b369 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -75,7 +75,8 @@ module LoginHelpers def logout find(".header-user-dropdown-toggle").click click_link "Sign out" - expect(page).to have_content('Signed out successfully') + # check the sign_in button + expect(page).to have_button('Sign in') end # Logout without JavaScript driver From 81a12c10fe7ba6f58ab4d1ae57861aa8899d545f Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Mon, 12 Dec 2016 09:55:29 +0100 Subject: [PATCH 20/20] API: Fix groups filter --- lib/api/groups.rb | 11 ++++++++++- spec/requests/api/groups_spec.rb | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index fbf7513302b..105d3ee342e 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,7 +1,7 @@ module API class Groups < Grape::API include PaginationParams - + before { authenticate! } helpers do @@ -117,11 +117,20 @@ module API success Entities::Project end params do + optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' + optional :visibility, type: String, values: %w[public internal private], + desc: 'Limit by visibility' + optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], + default: 'created_at', desc: 'Return projects ordered by field' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return projects sorted in ascending and descending order' use :pagination end get ":id/projects" do group = find_group!(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) + projects = filter_projects(projects) present paginate(projects), with: Entities::Project, user: current_user end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 548ed8e1892..15647b262b6 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -245,6 +245,17 @@ describe API::Groups, api: true do expect(project_names).to match_array([project1.name, project3.name]) end + it 'filters the groups projects' do + public_projet = create(:project, :public, path: 'test1', group: group1) + + get api("/groups/#{group1.id}/projects", user1), visibility: 'public' + + expect(response).to have_http_status(200) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(public_projet.name) + end + it "does not return a non existing group" do get api("/groups/1328/projects", user1) expect(response).to have_http_status(404)