From b36d84b32912bada57d8d6af9df64cb0a2478f7c Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 9 Jun 2016 16:06:13 +0100 Subject: [PATCH 01/50] Moved private forks notice to projects-list so its above the pagination and inline with list Updated CHANGELOG Removed CHANGELOG entry --- app/views/projects/forks/index.html.haml | 6 ------ app/views/shared/projects/_list.html.haml | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 4bcf2d9d533..dbe9ddfde2f 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -40,9 +40,3 @@ = render 'projects', projects: @forks - -- if @private_forks_count > 0 - .private-forks-notice - = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') - %strong= pluralize(@private_forks_count, 'private fork') - %span you have no access to. diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 2e08bb2ac08..3a9dd37dc7d 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -16,6 +16,12 @@ = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, forks: forks, show_last_commit_as_description: show_last_commit_as_description + + - if @private_forks_count && @private_forks_count > 0 + %li.project-row.private-forks-notice + = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') + %strong= pluralize(@private_forks_count, 'private fork') + %span you have no access to. = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages - else .nothing-here-block No projects found From 4652489f40a4ff2b749f9ad495986a7a17448243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 17 Jun 2016 14:06:55 +0200 Subject: [PATCH 02/50] New Members::DestroyService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is to ensure we don't send unwanted notifications when deleting a project. In other words, stop abusing AR callbacks and use services. Signed-off-by: Rémy Coutable --- .../concerns/membership_actions.rb | 6 ++- app/models/member.rb | 7 +-- app/models/members/group_member.rb | 12 ------ app/models/members/project_member.rb | 12 ------ app/services/members/destroy_service.rb | 33 ++++++++++++++ app/services/notification_service.rb | 21 +++------ spec/models/member_spec.rb | 12 ------ spec/models/members/group_member_spec.rb | 10 ----- spec/models/members/project_member_spec.rb | 10 ----- spec/services/members/destroy_service_spec.rb | 43 +++++++++++++++++++ 10 files changed, 88 insertions(+), 78 deletions(-) create mode 100644 app/services/members/destroy_service.rb create mode 100644 spec/services/members/destroy_service_spec.rb diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index a24273fad0b..b1343576b3c 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -23,22 +23,24 @@ module MembershipActions @member = membershipable.members.find_by(user_id: current_user) return render_403 unless @member + @member = Members::DestroyService.new(@member, current_user).execute + source_type = @member.real_source_type.humanize(capitalize: false) - if can?(current_user, action_member_permission(:destroy, @member), @member) + if @member.destroyed? notice = if @member.request? "Your access request to the #{source_type} has been withdrawn." else "You left the \"#{@member.source.human_name}\" #{source_type}." end - @member.destroy redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice else if cannot_leave? alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}." alert << " Transfer or delete the #{source_type}." + redirect_to polymorphic_url(membershipable), alert: alert else render_403 diff --git a/app/models/member.rb b/app/models/member.rb index 4ee3f1bb5c2..c74a16367db 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -48,7 +48,6 @@ class Member < ActiveRecord::Base after_create :post_create_hook, unless: [:pending?, :importing?] after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? - after_destroy :post_decline_request, if: :request? delegate :name, :username, :email, to: :user, prefix: true @@ -188,7 +187,7 @@ class Member < ActiveRecord::Base end def send_request - # override in subclass + notification_service.new_access_request(self) end def post_create_hook @@ -215,10 +214,6 @@ class Member < ActiveRecord::Base post_create_hook end - def post_decline_request - # override in subclass - end - def system_hook_service SystemHooksService.new end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 363db877968..2f13d339c89 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -33,12 +33,6 @@ class GroupMember < Member super end - def send_request - notification_service.new_group_access_request(self) - - super - end - def post_create_hook notification_service.new_group_member(self) @@ -64,10 +58,4 @@ class GroupMember < Member super end - - def post_decline_request - notification_service.decline_group_access_request(self) - - super - end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 250ee04fd1d..e9d3a82ba15 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -111,12 +111,6 @@ class ProjectMember < Member super end - def send_request - notification_service.new_project_access_request(self) - - super - end - def post_create_hook unless owner? event_service.join_project(self.project, self.user) @@ -152,12 +146,6 @@ class ProjectMember < Member super end - def post_decline_request - notification_service.decline_project_access_request(self) - - super - end - def event_service EventCreateService.new end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb new file mode 100644 index 00000000000..e32eb47b846 --- /dev/null +++ b/app/services/members/destroy_service.rb @@ -0,0 +1,33 @@ +module Members + class DestroyService < BaseService + attr_accessor :member, :current_user + + def initialize(member, user) + @member, @current_user = member, user + end + + def execute + if can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) + member.destroy + + notification_service.decline_access_request(member) if member.request? + end + + member + end + + private + + def abilities + Ability.abilities + end + + def can?(object, action, subject) + abilities.allowed?(object, action, subject) + end + + def notification_service + NotificationService.new + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 19832a19b2b..590350a11e5 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -181,15 +181,16 @@ class NotificationService end end - # Project access request - def new_project_access_request(project_member) - mailer.member_access_requested_email(project_member.real_source_type, project_member.id).deliver_later + # Members + def new_access_request(member) + mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later end - def decline_project_access_request(project_member) - mailer.member_access_denied_email(project_member.real_source_type, project_member.project.id, project_member.user.id).deliver_later + def decline_access_request(member) + mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later end + # Project invite def invite_project_member(project_member, token) mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later end @@ -216,15 +217,7 @@ class NotificationService mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later end - # Group access request - def new_group_access_request(group_member) - mailer.member_access_requested_email(group_member.real_source_type, group_member.id).deliver_later - end - - def decline_group_access_request(group_member) - mailer.member_access_denied_email(group_member.real_source_type, group_member.group.id, group_member.user.id).deliver_later - end - + # Group invite def invite_group_member(group_member, token) mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 3ed3202ac6c..e9134a3d283 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -134,18 +134,6 @@ describe Member, models: true do it { is_expected.to respond_to(:user_email) } end - describe 'Callbacks' do - describe 'after_destroy :post_decline_request, if: :request?' do - let(:member) { create(:project_member, requested_at: Time.now.utc) } - - it 'calls #post_decline_request' do - expect(member).to receive(:post_decline_request) - - member.destroy - end - end - end - describe ".add_user" do let!(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index eeb74a462ac..18439cac2a4 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -61,16 +61,6 @@ describe GroupMember, models: true do end end - describe '#post_decline_request' do - it 'calls NotificationService.decline_group_access_request' do - member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now) - - expect_any_instance_of(NotificationService).to receive(:decline_group_access_request) - - member.__send__(:post_decline_request) - end - end - describe '#real_source_type' do subject { create(:group_member).real_source_type } diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 1e466f9c620..bbf65edb27c 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -152,15 +152,5 @@ describe ProjectMember, models: true do member.__send__(:after_accept_request) end end - - describe '#post_decline_request' do - it 'calls NotificationService.decline_project_access_request' do - member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now) - - expect_any_instance_of(NotificationService).to receive(:decline_project_access_request) - - member.__send__(:post_decline_request) - end - end end end diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb new file mode 100644 index 00000000000..aa002b4bd22 --- /dev/null +++ b/spec/services/members/destroy_service_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Members::DestroyService, services: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let!(:member) { create(:project_member, source: project) } + + context 'when current user cannot destroy the given member' do + before do + project.team << [user, :developer] + end + + it 'does not destroy the member' do + expect(destroy_member(member, user)).not_to be_destroyed + end + end + + context 'when current user can destroy the given member' do + before do + project.team << [user, :master] + end + + it 'destroys the member' do + expect(destroy_member(member, user)).to be_destroyed + end + + context 'when the given member is a requester' do + before do + member.update_column(:requested_at, Time.now) + end + + it 'calls Member#after_decline_request' do + expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) + + destroy_member(member, user) + end + end + end + + def destroy_member(member, user) + Members::DestroyService.new(member, user).execute + end +end From 6c5b2377f769185f562578791139a13939867b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 17 Jun 2016 14:08:02 +0200 Subject: [PATCH 03/50] Use the new Members::DestroyService in group/project member controllers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/groups/group_members_controller.rb | 2 +- app/controllers/projects/project_members_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index d0f2e2949f0..c3929ded6dd 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -38,7 +38,7 @@ class Groups::GroupMembersController < Groups::ApplicationController return render_403 unless can?(current_user, :destroy_group_member, @group_member) - @group_member.destroy + Members::DestroyService.new(@group_member, current_user).execute respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 35d067cd029..ff0ac115f55 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -52,7 +52,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController return render_403 unless can?(current_user, :destroy_project_member, @project_member) - @project_member.destroy + Members::DestroyService.new(@project_member, current_user).execute respond_to do |format| format.html do From 724f986fb2cf8fee3606bffd01da9842634400ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 17 Jun 2016 16:33:10 +0200 Subject: [PATCH 04/50] Redirect to the member's source on request withdrawal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/concerns/membership_actions.rb | 3 ++- spec/controllers/groups/group_members_controller_spec.rb | 2 +- spec/controllers/projects/project_members_controller_spec.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index b1343576b3c..7e6b83bcc37 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -34,8 +34,9 @@ module MembershipActions else "You left the \"#{@member.source.human_name}\" #{source_type}." end + redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] - redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice + redirect_to redirect_path, notice: notice else if cannot_leave? alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}." diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 89c2c26a367..8798d709f30 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -134,7 +134,7 @@ describe Groups::GroupMembersController do delete :leave, group_id: group expect(response).to set_flash.to 'Your access request to the group has been withdrawn.' - expect(response).to redirect_to(dashboard_groups_path) + expect(response).to redirect_to(group_path(group)) expect(group.members.request).to be_empty expect(group.users).not_to include user end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index fc5f458e795..0a8fe5e04c3 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -190,7 +190,7 @@ describe Projects::ProjectMembersController do project_id: project expect(response).to set_flash.to 'Your access request to the project has been withdrawn.' - expect(response).to redirect_to(dashboard_projects_path) + expect(response).to redirect_to(namespace_project_path(project.namespace, project)) expect(project.members.request).to be_empty expect(project.users).not_to include user end From a08a26ac814d7fd9f7523e22847fab0cc25ceb78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 17 Jun 2016 16:33:37 +0200 Subject: [PATCH 05/50] Don't send the "access declined" email on access request withdrawal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/services/members/destroy_service.rb | 4 +++- spec/services/members/destroy_service_spec.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index e32eb47b846..59a55e42e38 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -10,7 +10,9 @@ module Members if can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) member.destroy - notification_service.decline_access_request(member) if member.request? + if member.request? && member.user != current_user + notification_service.decline_access_request(member) + end end member diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index aa002b4bd22..04c2782c125 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -34,6 +34,14 @@ describe Members::DestroyService, services: true do destroy_member(member, user) end + + context 'when current user is the member' do + it 'does not call Member#after_decline_request' do + expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) + + destroy_member(member, member.user) + end + end end end From 654565c9dc734a597c525a75c8f72dd63235604b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 17 Jun 2016 18:59:33 +0200 Subject: [PATCH 06/50] Raise a new Gitlab::Access::AccessDeniedError when permission is not enough to destroy a member MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a try for a new approach to put the access checks at the service level. Signed-off-by: Rémy Coutable --- app/controllers/application_controller.rb | 4 +++ .../concerns/membership_actions.rb | 34 +++++-------------- .../groups/group_members_controller.rb | 6 ---- .../projects/project_members_controller.rb | 6 ---- app/services/members/destroy_service.rb | 26 ++++---------- lib/gitlab/access.rb | 2 ++ spec/services/members/destroy_service_spec.rb | 24 +++++++++++-- 7 files changed, 42 insertions(+), 60 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dd1bc6f5d52..9cc31620d9f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -36,6 +36,10 @@ class ApplicationController < ActionController::Base render_404 end + rescue_from Gitlab::Access::AccessDeniedError do |exception| + render_403 + end + def redirect_back_or_default(default: root_path, options: {}) redirect_to request.referer.present? ? :back : default, options end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 7e6b83bcc37..52dc396af6a 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -21,32 +21,18 @@ module MembershipActions def leave @member = membershipable.members.find_by(user_id: current_user) - return render_403 unless @member - - @member = Members::DestroyService.new(@member, current_user).execute + Members::DestroyService.new(@member, current_user).execute source_type = @member.real_source_type.humanize(capitalize: false) - - if @member.destroyed? - notice = - if @member.request? - "Your access request to the #{source_type} has been withdrawn." - else - "You left the \"#{@member.source.human_name}\" #{source_type}." - end - redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] - - redirect_to redirect_path, notice: notice - else - if cannot_leave? - alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}." - alert << " Transfer or delete the #{source_type}." - - redirect_to polymorphic_url(membershipable), alert: alert + notice = + if @member.request? + "Your access request to the #{source_type} has been withdrawn." else - render_403 + "You left the \"#{@member.source.human_name}\" #{source_type}." end - end + redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] + + redirect_to redirect_path, notice: notice end protected @@ -54,8 +40,4 @@ module MembershipActions def membershipable raise NotImplementedError end - - def cannot_leave? - raise NotImplementedError - end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index c3929ded6dd..2c49fe3833e 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -36,8 +36,6 @@ class Groups::GroupMembersController < Groups::ApplicationController def destroy @group_member = @group.group_members.find(params[:id]) - return render_403 unless can?(current_user, :destroy_group_member, @group_member) - Members::DestroyService.new(@group_member, current_user).execute respond_to do |format| @@ -68,8 +66,4 @@ class Groups::GroupMembersController < Groups::ApplicationController # MembershipActions concern alias_method :membershipable, :group - - def cannot_leave? - @group.last_owner?(current_user) - end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index ff0ac115f55..6ba32d33403 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -50,8 +50,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController def destroy @project_member = @project.project_members.find(params[:id]) - return render_403 unless can?(current_user, :destroy_project_member, @project_member) - Members::DestroyService.new(@project_member, current_user).execute respond_to do |format| @@ -98,8 +96,4 @@ class Projects::ProjectMembersController < Projects::ApplicationController # MembershipActions concern alias_method :membershipable, :project - - def cannot_leave? - current_user == @project.owner - end end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 59a55e42e38..15358f80208 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -7,29 +7,15 @@ module Members end def execute - if can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) - member.destroy - - if member.request? && member.user != current_user - notification_service.decline_access_request(member) - end + unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) + raise Gitlab::Access::AccessDeniedError end - member - end + member.destroy - private - - def abilities - Ability.abilities - end - - def can?(object, action, subject) - abilities.allowed?(object, action, subject) - end - - def notification_service - NotificationService.new + if member.request? && member.user != current_user + notification_service.decline_access_request(member) + end end end end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 6d0e30e916f..831f1e635ba 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -5,6 +5,8 @@ # module Gitlab module Access + class AccessDeniedError < StandardError; end + GUEST = 10 REPORTER = 20 DEVELOPER = 30 diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 04c2782c125..2395445e7fd 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -5,13 +5,23 @@ describe Members::DestroyService, services: true do let(:project) { create(:project) } let!(:member) { create(:project_member, source: project) } + context 'when member is nil' do + before do + project.team << [user, :developer] + end + + it 'does not destroy the member' do + expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + context 'when current user cannot destroy the given member' do before do project.team << [user, :developer] end it 'does not destroy the member' do - expect(destroy_member(member, user)).not_to be_destroyed + expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError) end end @@ -21,7 +31,9 @@ describe Members::DestroyService, services: true do end it 'destroys the member' do - expect(destroy_member(member, user)).to be_destroyed + destroy_member(member, user) + + expect(member).to be_destroyed end context 'when the given member is a requester' do @@ -42,6 +54,14 @@ describe Members::DestroyService, services: true do destroy_member(member, member.user) end end + + context 'when current user is the member and ' do + it 'does not call Member#after_decline_request' do + expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) + + destroy_member(member, member.user) + end + end end end From bceee987134ad578b3d6f89c9bc61bef8e0c6066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Sat, 18 Jun 2016 05:44:15 +0200 Subject: [PATCH 07/50] Show 'Leave project' only if member can actually leave the project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/views/layouts/nav/_project.html.haml | 11 ++++++----- .../layouts/nav/_project_settings.html.haml | 2 +- .../members/member_leaves_project_spec.rb | 19 +++++++++++++++++++ .../owner_cannot_leave_project_spec.rb | 16 ++++++++++++++++ .../members/user_requests_access_spec.rb | 1 + 5 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 spec/features/projects/members/member_leaves_project_spec.rb create mode 100644 spec/features/projects/members/owner_cannot_leave_project_spec.rb diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 39ea4920ccc..a9289a8552a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -5,19 +5,20 @@ = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - - is_project_member = @project.users.exists?(current_user.id) - - access = @project.team.max_member_access(current_user.id) - 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.non_request.find_by(user_id: current_user.id) + - can_leave = member && can?(current_user, :destroy_project_member, member) - = render 'layouts/nav/project_settings', access: access, can_edit: can_edit + = render 'layouts/nav/project_settings', can_edit: can_edit - - if can_edit || is_project_member + - if can_edit || can_leave %li.divider - if can_edit %li = link_to edit_project_path(@project) do Edit Project - - if is_project_member + - if can_leave %li = link_to polymorphic_path([:leave, @project, :members]), data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 13d32bd1354..51a54b4f262 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -3,7 +3,7 @@ = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do %span Members -- if access && can_edit +- if can_edit - if @project.allowed_to_share_with_group? = nav_link(controller: :group_links) do = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb new file mode 100644 index 00000000000..79dec442818 --- /dev/null +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +feature 'Projects > Members > Member leaves project', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + + background do + project.team << [user, :developer] + login_as(user) + visit namespace_project_path(project.namespace, project) + end + + scenario 'user leaves project' do + click_link 'Leave Project' + + expect(current_path).to eq(dashboard_projects_path) + expect(project.users.exists?(user.id)).to be_falsey + end +end diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb new file mode 100644 index 00000000000..67811b1048e --- /dev/null +++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +feature 'Projects > Members > Owner cannot leave project', feature: true do + let(:owner) { create(:user) } + let(:project) { create(:project) } + + background do + project.team << [owner, :owner] + login_as(owner) + 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' + end +end diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index fd92a3a2f0c..af420c170ef 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -21,6 +21,7 @@ feature 'Projects > 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 Project' end scenario 'user is not listed in the project members page' do From bf05ca88eefb38bb52fb6e31622f9c1a795965e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Sat, 18 Jun 2016 05:45:52 +0200 Subject: [PATCH 08/50] Add 'Leave Group' link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The link was removed in !3798, probably by mistake. Signed-off-by: Rémy Coutable --- .../layouts/nav/_group_settings.html.haml | 36 +++++++++++-------- .../last_owner_cannot_leave_group_spec.rb | 16 +++++++++ .../members/member_leaves_group_spec.rb | 22 ++++++++++++ .../members/user_requests_access_spec.rb | 1 + 4 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 spec/features/groups/members/last_owner_cannot_leave_group_spec.rb create mode 100644 spec/features/groups/members/member_leaves_group_spec.rb diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index dac46648b9f..3a24b09ab7e 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,16 +1,22 @@ - if current_user - - if access = @group.users.find_by(id: current_user.id) - .controls - .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - if can?(current_user, :admin_group, @group) - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do - Projects - %li.divider - %li - = link_to edit_group_path(@group) do - Edit Group + - can_edit = can?(current_user, :admin_group, @group) + - member = @group.members.non_request.find_by(user_id: current_user.id) + - can_leave = member && can?(current_user, :destroy_group_member, member) + + .controls + .dropdown.group-settings-dropdown + %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + = nav_link(path: 'groups#projects') do + = link_to 'Projects', projects_group_path(@group), title: 'Projects' + %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/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb new file mode 100644 index 00000000000..33bf6d3752f --- /dev/null +++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +feature 'Groups > Members > Last owner cannot leave group', feature: true do + let(:owner) { create(:user) } + let(:group) { create(:group) } + + background do + group.add_owner(owner) + login_as(owner) + visit group_path(group) + end + + 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 new file mode 100644 index 00000000000..04dfb22db49 --- /dev/null +++ b/spec/features/groups/members/member_leaves_group_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'Groups > Members > Member leaves group', feature: true do + let(:user) { create(:user) } + let(:owner) { create(:user) } + let(:group) { create(:group, :public) } + + background do + group.add_owner(owner) + group.add_developer(user) + login_as(user) + visit group_path(group) + end + + scenario 'user leaves group' do + # find('#group-settings-button').click + click_link 'Leave Group' + + expect(current_path).to eq(dashboard_groups_path) + expect(group.users.exists?(user.id)).to be_falsey + end +end diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb index a878a96b6ee..1ea607cbca0 100644 --- a/spec/features/groups/members/user_requests_access_spec.rb +++ b/spec/features/groups/members/user_requests_access_spec.rb @@ -21,6 +21,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' end scenario 'user is not listed in the group members page' do From 34e313920d8dc57dfdc92e3740fb30f0a32ebd71 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 20 Jun 2016 09:47:43 +0100 Subject: [PATCH 09/50] Fixed hover of date picker calendar --- app/assets/stylesheets/framework/dropdowns.scss | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d4d579a083d..00111dfa706 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -461,10 +461,12 @@ } } - .ui-state-active, - .ui-state-hover { - color: $md-link-color; - background-color: $calendar-hover-bg; + .ui-datepicker-calendar { + .ui-state-hover, + .ui-state-active { + color: #fff; + border: 0; + } } .ui-datepicker-prev, From 608271626d44c950fffc96ab54ecdaf60bc5f490 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 20 Jun 2016 12:50:36 +0300 Subject: [PATCH 10/50] Fix admin appearance settings preview Signed-off-by: Dmitriy Zaporozhets --- .../admin/appearances_controller.rb | 1 + app/views/admin/appearances/_form.html.haml | 2 +- app/views/admin/appearances/preview.html.haml | 34 ++++--------------- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index 26cf74e4849..4b0ec54b3f4 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -5,6 +5,7 @@ class Admin::AppearancesController < Admin::ApplicationController end def preview + render 'preview', layout: 'devise' end def create diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index d88f3ad314d..dc083e50178 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -46,7 +46,7 @@ Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo .form-actions - = f.submit 'Save', class: 'btn btn-save' + = f.submit 'Save', class: 'btn btn-save append-right-10' - if @appearance.persisted? = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank' diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml index dd4a64e80bc..6c51639b840 100644 --- a/app/views/admin/appearances/preview.html.haml +++ b/app/views/admin/appearances/preview.html.haml @@ -1,29 +1,9 @@ - page_title "Preview | Appearance" -%h3.page-title - Appearance settings - Preview -%hr +.login-box + .login-heading + %h3 Existing user? Sign in + %form + = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email" + = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password" + = button_tag "Sign in", class: "btn-create btn" -.ui-box - .title - Sign-in page - %div - .login-page - .container - .content - .login-title - %h1= brand_title - %hr - .container - .content - .row - .col-sm-7 - .brand-image - = brand_image - .brand_text - = brand_text - .col-sm-4 - .login-box - %h3.page-title Sign in - = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email" - = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password" - = button_tag "Sign in", class: "btn-create btn" From 909a0ff3ace1eb82a4296764777a552779c39839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 20 Jun 2016 12:36:59 +0200 Subject: [PATCH 11/50] Fix and remove duplicate specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- features/dashboard/group.feature | 44 ------------------- features/steps/dashboard/group.rb | 42 ------------------ .../groups/group_members_controller_spec.rb | 4 +- .../project_members_controller_spec.rb | 6 +-- .../members/member_leaves_group_spec.rb | 1 - spec/features/projects_spec.rb | 16 ------- 6 files changed, 2 insertions(+), 111 deletions(-) diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature index e3c01db2ebb..3ae2c679dc1 100644 --- a/features/dashboard/group.feature +++ b/features/dashboard/group.feature @@ -5,53 +5,9 @@ Feature: Dashboard Group And "John Doe" is owner of group "Owned" And "John Doe" is guest of group "Guest" - # Leave groups - - @javascript - Scenario: Owner should be able to leave from group if he is not the last owner - Given "Mary Jane" is owner of group "Owned" - When I visit dashboard groups page - Then I should see group "Owned" in group list - Then I should see group "Guest" in group list - When I click on the "Leave" button for group "Owned" - And I visit dashboard groups page - Then I should not see group "Owned" in group list - Then I should see group "Guest" in group list - - @javascript - Scenario: Owner should not be able to leave from group if he is the last owner - Given "Mary Jane" is guest of group "Owned" - When I visit dashboard groups page - Then I should see group "Owned" in group list - Then I should see group "Guest" in group list - When I click on the "Leave" button for group "Owned" - Then I should see the "Can not leave message" - - @javascript - Scenario: Guest should be able to leave from group - Given "Mary Jane" is guest of group "Guest" - When I visit dashboard groups page - Then I should see group "Owned" in group list - Then I should see group "Guest" in group list - When I click on the "Leave" button for group "Guest" - When I visit dashboard groups page - Then I should see group "Owned" in group list - Then I should not see group "Guest" in group list - - @javascript - Scenario: Guest should be able to leave from group even if he is the only user in the group - When I visit dashboard groups page - Then I should see group "Owned" in group list - Then I should see group "Guest" in group list - When I click on the "Leave" button for group "Guest" - When I visit dashboard groups page - Then I should see group "Owned" in group list - Then I should not see group "Guest" in group list - Scenario: Create a group from dasboard And I visit dashboard groups page And I click new group link And submit form with new group "Samurai" info Then I should be redirected to group "Samurai" page And I should see newly created group "Samurai" - diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb index 9b79a3be49b..cf679fea530 100644 --- a/features/steps/dashboard/group.rb +++ b/features/steps/dashboard/group.rb @@ -4,44 +4,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps include SharedPaths include SharedUser - # Leave - - step 'I click on the "Leave" button for group "Owned"' do - find(:css, 'li', text: "Owner").find(:css, 'i.fa.fa-sign-out').click - # poltergeist always confirms popups. - end - - step 'I click on the "Leave" button for group "Guest"' do - find(:css, 'li', text: "Guest").find(:css, 'i.fa.fa-sign-out').click - # poltergeist always confirms popups. - end - - step 'I should not see the "Leave" button for group "Owned"' do - expect(find(:css, 'li', text: "Owner")).not_to have_selector(:css, 'i.fa.fa-sign-out') - # poltergeist always confirms popups. - end - - step 'I should not see the "Leave" button for groupr "Guest"' do - expect(find(:css, 'li', text: "Guest")).not_to have_selector(:css, 'i.fa.fa-sign-out') - # poltergeist always confirms popups. - end - - step 'I should see group "Owned" in group list' do - expect(page).to have_content("Owned") - end - - step 'I should not see group "Owned" in group list' do - expect(page).not_to have_content("Owned") - end - - step 'I should see group "Guest" in group list' do - expect(page).to have_content("Guest") - end - - step 'I should not see group "Guest" in group list' do - expect(page).not_to have_content("Guest") - end - step 'I click new group link' do click_link "New Group" end @@ -60,8 +22,4 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps expect(page).to have_content "Samurai" expect(page).to have_content "Tokugawa Shogunate" end - - step 'I should see the "Can not leave message"' do - expect(page).to have_content "You can not leave the \"Owned\" group." - end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 8798d709f30..c8601341d54 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -118,9 +118,7 @@ describe Groups::GroupMembersController do it 'cannot removes himself from the group' do delete :leave, group_id: group - expect(response).to redirect_to(group_path(group)) - expect(response).to set_flash[:alert].to "You can not leave the \"#{group.name}\" group. Transfer or delete the group." - expect(group.users).to include user + expect(response.status).to eq(403) end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 0a8fe5e04c3..e5e750c855f 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -171,11 +171,7 @@ describe Projects::ProjectMembersController do delete :leave, namespace_id: project.namespace, project_id: project - expect(response).to redirect_to( - namespace_project_path(project.namespace, project) - ) - expect(response).to set_flash[:alert].to "You can not leave the \"#{project.human_name}\" project. Transfer or delete the project." - expect(project.users).to include user + expect(response.status).to eq(403) 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 04dfb22db49..3185ff924b9 100644 --- a/spec/features/groups/members/member_leaves_group_spec.rb +++ b/spec/features/groups/members/member_leaves_group_spec.rb @@ -13,7 +13,6 @@ feature 'Groups > Members > Member leaves group', feature: true do end scenario 'user leaves group' do - # find('#group-settings-button').click click_link 'Leave Group' expect(current_path).to eq(dashboard_groups_path) diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 9dd0378d165..6fa8298d489 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -70,22 +70,6 @@ feature 'Project', feature: true do end end - describe 'leave project link' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - - before do - login_with(user) - project.team.add_user(user, Gitlab::Access::MASTER) - visit namespace_project_path(project.namespace, project) - end - - it 'click project-settings and find leave project' do - find('#project-settings-button').click - expect(page).to have_link('Leave Project') - end - end - describe 'project title' do include WaitForAjax From 043522a8aed5e598f3cfe97480a5b169fe0786f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 20 Jun 2016 15:50:46 +0200 Subject: [PATCH 12/50] Display group/project access requesters separately in admin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, reuse partials from the non-admin views. Signed-off-by: Rémy Coutable --- app/views/admin/groups/show.html.haml | 33 +++++-------- app/views/admin/projects/show.html.haml | 49 ++++++------------- .../groups/group_members/index.html.haml | 3 +- .../project_members/_group_members.html.haml | 3 +- .../_shared_group_members.html.haml | 5 +- .../projects/project_members/_team.html.haml | 3 +- .../projects/project_members/index.html.haml | 2 +- app/views/shared/members/_requests.html.haml | 2 +- 8 files changed, 35 insertions(+), 65 deletions(-) diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 5b8a0262ea0..50770465f07 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -88,28 +88,17 @@ = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr = button_tag 'Add users to group', class: "btn btn-create" + + = render 'shared/members/requests', membership_source: @group, members: @members.request + .panel.panel-default .panel-heading - %h3.panel-title - Members - %span.badge - #{@group.group_members.count} - %ul.well-list.group-users-list - - @members.each do |member| - - user = member.user - %li{class: dom_class(member), id: (dom_id(user) if user)} - .list-item-name - - if user - %strong - = link_to user.name, admin_user_path(user) - - else - %strong - = member.invite_email - (invited) - %span.pull-right.light - = member.human_access - - if can?(current_user, :destroy_group_member, member) - = link_to group_group_member_path(@group, member), data: { confirm: remove_member_message(member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + %strong= @group.name + group members + %span.badge= @group.members.non_request.size + .pull-right + = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@group, :members]), class: "btn btn-xs" + %ul.well-list.group-users-list.content-list + = render partial: 'shared/members/member', collection: @members.non_request, as: :member, locals: { show_controls: false } .panel-footer - = paginate @members, param_name: 'members_page', theme: 'gitlab' + = paginate @members.non_request, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 9e55a562e18..461d588415d 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -135,44 +135,27 @@ - if @group .panel.panel-default .panel-heading - %strong #{@group.name} - group members (#{@group.group_members.count}) + %strong= @group.name + group members + %span.badge= @group_members.non_request.size .pull-right = link_to admin_group_path(@group), class: 'btn btn-xs' do - %i.fa.fa-pencil-square-o - %ul.well-list - - @group_members.each do |member| - = render 'shared/members/member', member: member, show_controls: false + = icon('pencil-square-o', text: 'Manage Access') + %ul.well-list.content-list + = render partial: 'shared/members/member', collection: @group_members.non_request, as: :member, locals: { show_controls: false } .panel-footer - = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab' + = paginate @group_members.non_request, param_name: 'group_members_page', theme: 'gitlab' + + = render 'shared/members/requests', membership_source: @project, members: @project_members.request .panel.panel-default .panel-heading - Project members - %small - (#{@project.users.count}) + %strong= @project.name + project members + %span.badge= @project.users.size .pull-right - = link_to namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-xs" do - %i.fa.fa-pencil-square-o - Manage Access - %ul.well-list.project_members - - @project_members.each do |project_member| - - user = project_member.user - %li.project_member - .list-item-name - - if user - %strong - = link_to user.name, admin_user_path(user) - - else - %strong - = project_member.invite_email - (invited) - .pull-right - - if project_member.owner? - %span.light Owner - - else - %span.light= project_member.human_access - = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_member_message(project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do - %i.fa.fa-times + = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@project, :members]), class: "btn btn-xs" + %ul.well-list.project_members.content-list + = render partial: 'shared/members/member', collection: @project_members.non_request, as: :member, locals: { show_controls: false } .panel-footer - = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' + = paginate @project_members.non_request, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index a36531e095a..d6acade84f1 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -17,8 +17,7 @@ .panel-heading %strong #{@group.name} group members - %small - (#{@members.total_count}) + %span.badge= @members.non_request.size .controls = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index cb6136c215a..e783d8c72c5 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -2,8 +2,7 @@ .panel-heading %strong #{@group.name} group members - %small - (#{members.count}) + %span.badge= members.size - if can?(current_user, :admin_group_member, @group) .controls = link_to 'Manage group members', diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml index 952844acefc..840b57c2e63 100644 --- a/app/views/projects/project_members/_shared_group_members.html.haml +++ b/app/views/projects/project_members/_shared_group_members.html.haml @@ -1,6 +1,7 @@ - @project_group_links.each do |group_links| - shared_group = group_links.group - - shared_group_users_count = group_links.group.group_members.count + - shared_group_members = shared_group.members.non_request + - shared_group_users_count = shared_group_members.size .panel.panel-default .panel-heading Shared with @@ -15,7 +16,7 @@ Edit group members %ul.content-list = render partial: 'shared/members/member', - collection: shared_group.group_members.order(access_level: :desc).limit(20), + collection: shared_group_members.order(access_level: :desc).limit(20), as: :member, locals: { show_controls: false, show_roles: false } - if shared_group_users_count > 20 diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 03207614258..b0bfdd235f7 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -2,8 +2,7 @@ .panel-heading %strong #{@project.name} project members - %small - (#{members.count}) + %span.badge= members.size .controls = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do .form-group diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 357ccccaf1d..a2026c41d01 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -18,7 +18,7 @@ = render 'team', members: @project_members.non_request - if @group - = render "group_members", members: @group_members + = render "group_members", members: @group_members.non_request - if @project_group_links.any? && @project.allowed_to_share_with_group? = render "shared_group_members" diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index b5963876034..e4bd2bdc265 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -3,6 +3,6 @@ .panel-heading %strong= membership_source.name access requests - %small= "(#{members.size})" + %span.badge= members.size %ul.content-list = render partial: 'shared/members/member', collection: members, as: :member From 7c41f35953494f5f7372a434c8c62a7b466c8212 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 20 Jun 2016 09:32:01 -0500 Subject: [PATCH 13/50] Add fade divs as li elements --- app/views/layouts/nav/_admin.html.haml | 4 ++-- app/views/layouts/nav/_group.html.haml | 4 ++-- app/views/layouts/nav/_profile.html.haml | 4 ++-- app/views/layouts/nav/_project.html.haml | 4 ++-- app/views/projects/commits/_head.html.haml | 4 ++-- app/views/shared/_event_filter.html.haml | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 4722c9d9353..ffdc7b7f504 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -2,7 +2,7 @@ = render 'layouts/nav/admin_settings' %ul.nav-links.scrolling-tabs - .fade-left + %li.fade-left = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span @@ -36,4 +36,4 @@ = link_to admin_spam_logs_path, title: "Spam Logs" do %span Spam Logs - .fade-right + %li.fade-right diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 66361a644dd..3bd04c2c8b4 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -2,7 +2,7 @@ = render 'layouts/nav/group_settings' %ul.nav-links.scrolling-tabs - .fade-left + %li.fade-left = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do %span @@ -31,4 +31,4 @@ = link_to group_group_members_path(@group), title: 'Members' do %span Members - .fade-right + %li.fade-right diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index bb6f14a6225..110a72d3a3c 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,5 +1,5 @@ %ul.nav-links.scrolling-tabs - .fade-left + %li.fade-left = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do %span @@ -43,4 +43,4 @@ = link_to audit_log_profile_path, title: 'Audit Log' do %span Audit Log - .fade-right + %li.fade-right diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 39ea4920ccc..823051d1b83 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -25,7 +25,7 @@ %div{ class: nav_control_class } %ul.nav-links.scrolling-tabs - .fade-left + %li.fade-left = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do %span @@ -109,4 +109,4 @@ %li.hidden = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do Commits - .fade-right + %li.fade-right diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index c8aa849c217..888c6b6701b 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,7 +1,7 @@ .scrolling-tabs-container .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } - .fade-left + %li.fade-left = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project) do Files @@ -25,4 +25,4 @@ = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - .fade-right + %li.fade-right diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 30055002213..6f9809a9ed8 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,7 +1,7 @@ %ul.nav-links.event-filter.scrolling-tabs - .fade-left + %li.fade-left = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' - .fade-right + %li.fade-right From 00ac7ae84a9d518e4d800973ac1056b720e86719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 20 Jun 2016 16:40:35 +0200 Subject: [PATCH 14/50] Fix specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../groups/members/owner_manages_access_requests_spec.rb | 2 +- .../projects/members/master_manages_access_requests_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb index 22525ce530b..321c9bad7d0 100644 --- a/spec/features/groups/members/owner_manages_access_requests_spec.rb +++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb @@ -42,7 +42,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do def expect_visible_access_request(group, user) expect(group.members.request.exists?(user_id: user)).to be_truthy - expect(page).to have_content "#{group.name} access requests (1)" + expect(page).to have_content "#{group.name} access requests 1" expect(page).to have_content user.name end end diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb index 5fe4caa12f0..aa2d906fa2e 100644 --- a/spec/features/projects/members/master_manages_access_requests_spec.rb +++ b/spec/features/projects/members/master_manages_access_requests_spec.rb @@ -41,7 +41,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do def expect_visible_access_request(project, user) expect(project.members.request.exists?(user_id: user)).to be_truthy - expect(page).to have_content "#{project.name} access requests (1)" + expect(page).to have_content "#{project.name} access requests 1" expect(page).to have_content user.name end end From e50739f4c7bf823f2c1a4a65363ed987eff7d399 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 20 Jun 2016 09:21:14 -0600 Subject: [PATCH 15/50] Apply responsive design for Contributors graphs. Fixes #18845. --- .../graphs/stat_graph_contributors_graph.js.coffee | 6 +++++- app/assets/stylesheets/pages/stat_graph.scss | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee index 584d281a510..834a81af459 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee @@ -121,7 +121,11 @@ class @ContributorsMasterGraph extends ContributorsGraph class @ContributorsAuthorGraph extends ContributorsGraph constructor: (@data) -> - @width = $('.content').width()/2 - 100 + # Don't split graph size in half for mobile devices. + if $(window).width() < 768 + @width = $('.content').width() - 80 + else + @width = ($('.content').width() / 2) - 100 @height = 200 @x = null @y = null diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 85a0304196c..8a1f2d098d6 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -25,13 +25,19 @@ &:nth-child(even) { float: right; } + float: left; margin-top: 10px; + + @media (max-width: $screen-sm-min) { + width: 100%; + } } .person .spark { display: block; background: #f3f3f3; + width: 100%; } .person .area-contributor { From 0273de3a0728822572f56201e9fe7b3dba510272 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 20 Jun 2016 14:11:15 -0500 Subject: [PATCH 16/50] Fixes labels view on mobile --- app/assets/stylesheets/pages/labels.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 046c38aba44..f5f67e2cd84 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -50,11 +50,10 @@ .label-row { .label-name { - display: block; + display: inline-block; margin-bottom: 10px; @media (min-width: $screen-sm-min) { - display: inline-block; width: 200px; margin-bottom: 0; } @@ -63,6 +62,7 @@ .label-description { display: block; margin-bottom: 10px; + margin-left: 50px; @media (min-width: $screen-sm-min) { display: inline-block; From 26d2fa0aaa4546ab6d41d08b4f0af66ca5d14ec0 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 20 Jun 2016 12:30:04 -0700 Subject: [PATCH 17/50] Rename Code tab to Repo Closes #18830 --- app/views/layouts/nav/_project.html.haml | 4 ++-- features/steps/project/project_find_file.rb | 4 ++-- features/steps/shared/project_tab.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 39ea4920ccc..068332205bb 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -38,9 +38,9 @@ - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do - = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do + = link_to project_files_path(@project), title: 'Repo', class: 'shortcuts-tree' do %span - Code + Repo - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :environments]) do diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 47de4b91df1..9833eec3730 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Code') + ensure_active_main_tab('Repo') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Code') + ensure_active_main_tab('Repo') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index bfee8793301..595913ff3d8 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,8 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Code' do - ensure_active_main_tab('Code') + step 'the active main tab should be Repo' do + ensure_active_main_tab('Repo') end step 'the active main tab should be Graphs' do From 5c9f0896ef3118b4e381e1834073827f42a10feb Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 20 Jun 2016 12:50:40 -0700 Subject: [PATCH 18/50] Rename Code -> Repo in feature specs --- features/project/active_tab.feature | 30 ++++++++++++++--------------- features/project/shortcuts.feature | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index c4f987a7923..db4507b9889 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,9 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Code + Scenario: On Project Repo Given I visit my project's files page - Then the active main tab should be Code + Then the active main tab should be Repo And no other main tabs should be active Scenario: On Project Issues @@ -59,46 +59,46 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Code + # Sub Tabs: Repo - Scenario: On Project Code/Files + Scenario: On Project Repo/Files Given I visit my project's files page Then the active sub tab should be Files And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repo - Scenario: On Project Code/Commits + Scenario: On Project Repo/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repo - Scenario: On Project Code/Network + Scenario: On Project Repo/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repo - Scenario: On Project Code/Compare + Scenario: On Project Repo/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repo - Scenario: On Project Code/Branches + Scenario: On Project Repo/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repo - Scenario: On Project Code/Tags + Scenario: On Project Repo/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Code + And the active main tab should be Repo Scenario: On Project Issues/Browse Given I visit my project's issues page diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index c73d0b32337..aa5edaaa9d7 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,21 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Code + Then the active main tab should be Repo Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Code + Then the active main tab should be Repo Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Code + And the active main tab should be Repo @javascript Scenario: Navigate to graphs tab From 3cf9b772d9eac98797ed12bfa00509ad8f4fa1d9 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Tue, 21 Jun 2016 00:21:56 +0300 Subject: [PATCH 19/50] jQuery selector refactor in application.js. --- app/assets/javascripts/application.js.coffee | 33 +++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 4529c514555..17b1cfda579 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -121,6 +121,11 @@ window.onload = -> setTimeout shiftWindow, 100 $ -> + + $document = $(document) + $window = $(window) + $body = $('body') + gl.utils.preventDisabledButtons() bootstrapBreakpoint = bp.getBreakpointSize() @@ -152,7 +157,7 @@ $ -> ), 1 # Initialize tooltips - $('body').tooltip( + $body.tooltip( selector: '.has-tooltip, [data-toggle="tooltip"]' placement: (_, el) -> $el = $(el) @@ -171,7 +176,7 @@ $ -> flash.show() # Disable form buttons while a form is submitting - $('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) -> + $body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) -> buttons = $('[type="submit"]', @) switch e.type @@ -184,7 +189,7 @@ $ -> $('.account-box').hover -> $(@).toggleClass('hover') # Commit show suppressed diff - $(document).on 'click', '.diff-content .js-show-suppressed-diff', -> + $document.on 'click', '.diff-content .js-show-suppressed-diff', -> $container = $(@).parent() $container.next('table').show() $container.remove() @@ -197,13 +202,13 @@ $ -> $('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left") # Show/hide comments on diff - $("body").on "click", ".js-toggle-diff-comments", (e) -> + $body.on "click", ".js-toggle-diff-comments", (e) -> $(@).toggleClass('active') $(@).closest(".diff-file").find(".notes_holder").toggle() e.preventDefault() - $(document).off "click", '.js-confirm-danger' - $(document).on "click", '.js-confirm-danger', (e) -> + $document.off "click", '.js-confirm-danger' + $document.on "click", '.js-confirm-danger', (e) -> e.preventDefault() btn = $(e.target) text = btn.data("confirm-danger-message") @@ -211,7 +216,7 @@ $ -> new ConfirmDangerModal(form, text) - $(document).on 'click', 'button', -> + $document.on 'click', 'button', -> $(this).blur() $('input[type="search"]').each -> @@ -219,7 +224,7 @@ $ -> $this.attr 'value', $this.val() return - $(document) + $document .off 'keyup', 'input[type="search"]' .on 'keyup', 'input[type="search"]' , (e) -> $this = $(this) @@ -227,7 +232,7 @@ $ -> $sidebarGutterToggle = $('.js-sidebar-toggle') - $(document) + $document .off 'breakpoint:change' .on 'breakpoint:change', (e, breakpoint) -> if breakpoint is 'sm' or breakpoint is 'xs' @@ -239,14 +244,14 @@ $ -> oldBootstrapBreakpoint = bootstrapBreakpoint bootstrapBreakpoint = bp.getBreakpointSize() if bootstrapBreakpoint != oldBootstrapBreakpoint - $(document).trigger('breakpoint:change', [bootstrapBreakpoint]) + $document.trigger('breakpoint:change', [bootstrapBreakpoint]) checkInitialSidebarSize = -> bootstrapBreakpoint = bp.getBreakpointSize() if bootstrapBreakpoint is "xs" or "sm" - $(document).trigger('breakpoint:change', [bootstrapBreakpoint]) + $document.trigger('breakpoint:change', [bootstrapBreakpoint]) - $(window) + $window .off "resize.app" .on "resize.app", (e) -> fitSidebarForSize() @@ -256,14 +261,14 @@ $ -> new Aside() # Sidenav pinning - if $(window).width() < 1440 and $.cookie('pin_nav') is 'true' + if $window.width() < 1440 and $.cookie('pin_nav') is 'true' $.cookie('pin_nav', 'false', { path: '/' }) $('.page-with-sidebar') .toggleClass('page-sidebar-collapsed page-sidebar-expanded') .removeClass('page-sidebar-pinned') $('.navbar-fixed-top').removeClass('header-pinned-nav') - $(document) + $document .off 'click', '.js-nav-pin' .on 'click', '.js-nav-pin', (e) -> e.preventDefault() From a0fefc2ad22ed2392bcba5acb02a0a95b73cbba8 Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Tue, 14 Jun 2016 17:58:20 -0700 Subject: [PATCH 20/50] Add definitions and tweak some docs. Partially fixes #17733 --- doc/ci/README.md | 1 + doc/ci/definitions/README.md | 52 ++++++++++++++++++++++++++++++++++++ doc/ci/quick_start/README.md | 24 ++++++++--------- doc/ci/yaml/README.md | 4 +-- 4 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 doc/ci/definitions/README.md diff --git a/doc/ci/README.md b/doc/ci/README.md index 5a1cb5319c6..c7a29e269b5 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -3,6 +3,7 @@ ### CI User documentation - [Get started with GitLab CI](quick_start/README.md) +- [CI/CD Definitions](definitions/README.md) - [CI examples for various languages](examples/README.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) - [Environments and deployments](environments.md) diff --git a/doc/ci/definitions/README.md b/doc/ci/definitions/README.md new file mode 100644 index 00000000000..4ec5eee5757 --- /dev/null +++ b/doc/ci/definitions/README.md @@ -0,0 +1,52 @@ +## CI/CD Definitions + +### Pipelines + +A pipeline is a group of [builds] that get executed in [stages] (batches). All of +the builds in a stage are executed in parallel (if there are enough concurrent +[runners]), and if they all succeed, the pipeline moves on to the next stage. If +one of the builds fails, the next stage is not (usually) executed. + +### Builds + +Builds are runs of [jobs]. Not to be confused with a `build` stage. + +### Jobs + +Jobs are the basic work unit of CI/CD. Jobs are used to create [builds], which are +then picked up by [Runners] and executed within the environment of the Runner. +Each job is run independently from each other. + +### Runners + +A runner is an isolated (virtual) machine that picks up builds through the +coordinator API of GitLab CI. A runner can be specific to a certain project or +serve any project in GitLab CI. A runner that serves all projects is called a +shared runner. + +### Stages + +Stages allow [jobs] to be grouped into parallel and sequential [builds]. Builds +of the same stage are executed in parallel and builds of the next stage are run +after the jobs from the previous stage complete successfully. Stages allow for +flexible multi-stage [pipelines]. By default [pipelines] have `build`, `test` +and `deploy` stages, but these can be defined in `.gitlab-ci.yml`. If a job +doesn't specify a stage, the job is assigned to the test stage. + +### Environments + +Environments are places where code gets deployed, such as staging or production. +CI/CD [Pipelines] usually have one or more deploy stages with [jobs] that do +[deployments] to an environment. + +### Deployments + +Deployments are created when [jobs] deploy versions of code to [environments]. + +[pipelines]: #pipelines +[builds]: #builds +[runners]: #runners +[jobs]: #jobs +[stages]: #stages +[environments]: #environments +[deployments]: #deployments diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 386b8e29fcf..07fbefa0416 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -4,41 +4,40 @@ is fully integrated into GitLab itself and is [enabled] by default on all projects. -The TL;DR version of how GitLab CI works is the following. - ---- - GitLab offers a [continuous integration][ci] service. If you [add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository, and configure your GitLab project to use a [Runner], then each merge request or -push triggers a build. +push triggers your CI [pipeline]. The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it -runs three [stages]: `build`, `test`, and `deploy`. +runs a pipeline with three [stages]: `build`, `test`, and `deploy`. If everything runs OK (no non-zero return values), you'll get a nice green checkmark associated with the pushed commit or merge request. This makes it -easy to see whether a merge request will cause any of the tests to fail before +easy to see whether a merge request caused any of the tests to fail before you even look at the code. -Most projects only use GitLab's CI service to run the test suite so that +Most projects use GitLab's CI service to run the test suite so that developers get immediate feedback if they broke something. +There's a growing trend to use continuous delivery and continuous deployment to +automatically deploy tested code to staging and production environments. + So in brief, the steps needed to have a working CI can be summed up to: 1. Add `.gitlab-ci.yml` to the root directory of your repository 1. Configure a Runner -From there on, on every push to your Git repository, the build will be -automagically started by the Runner and will appear under the project's -`/builds` page. +From there on, on every push to your Git repository, the Runner will +automagically start the pipeline and the pipeline will appear under the +project's `/pipelines` page. --- This guide assumes that you: - have a working GitLab instance of version 8.0 or higher or are using - [GitLab.com](https://gitlab.com/users/sign_in) + [GitLab.com](https://gitlab.com) - have a project in GitLab that you would like to use CI for Let's break it down to pieces and work on solving the GitLab CI puzzle. @@ -238,3 +237,4 @@ CI with various languages. [runner]: ../runners/README.md [enabled]: ../enable_or_disable_ci.md [stages]: ../yaml/README.md#stages +[pipeline]: ../definitions/README.md#pipelines diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d0fbcbe9988..b134b5cd5d3 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -54,7 +54,7 @@ of your repository and contains definitions of how your project should be built. The YAML file defines a set of jobs with constraints stating when they should be run. The jobs are defined as top-level elements with a name and always have -to contain the `script` clause: +to contain at least the `script` clause: ```yaml job1: @@ -165,7 +165,7 @@ stages: There are also two edge cases worth mentioning: -1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, +1. If no `stages` are defined in `.gitlab-ci.yml`, then by default the `build`, `test` and `deploy` are allowed to be used as job's stage by default. 2. If a job doesn't specify a `stage`, the job is assigned the `test` stage. From a1c2b168377869da8f3393c9a8f18a85633c3d57 Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Tue, 14 Jun 2016 18:12:39 -0700 Subject: [PATCH 21/50] Add pipeline image --- doc/ci/quick_start/README.md | 42 +++++++++++--------- doc/ci/quick_start/img/pipelines_status.png | Bin 0 -> 89387 bytes 2 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 doc/ci/quick_start/img/pipelines_status.png diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 07fbefa0416..c32b69aad8b 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -9,8 +9,9 @@ GitLab offers a [continuous integration][ci] service. If you and configure your GitLab project to use a [Runner], then each merge request or push triggers your CI [pipeline]. -The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it -runs a pipeline with three [stages]: `build`, `test`, and `deploy`. +The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs +a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to +use all three stages; stages with no jobs are simply ignored. If everything runs OK (no non-zero return values), you'll get a nice green checkmark associated with the pushed commit or merge request. This makes it @@ -56,15 +57,14 @@ On any push to your repository, GitLab will look for the `.gitlab-ci.yml` file and start builds on _Runners_ according to the contents of the file, for that commit. -Because `.gitlab-ci.yml` is in the repository, it is version controlled, -old versions still build successfully, forks can easily make use of CI, -branches can have separate builds and you have a single source of truth for CI. -You can read more about the reasons why we are using `.gitlab-ci.yml` -[in our blog about it][blog-ci]. +Because `.gitlab-ci.yml` is in the repository and is version controlled, old +versions still build successfully, forks can easily make use of CI, branches can +have different pipelines and jobs, and you have a single source of truth for CI. +You can read more about the reasons why we are using `.gitlab-ci.yml` [in our +blog about it][blog-ci]. **Note:** `.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file -so you have to pay extra attention to the indentation. Always use spaces, not -tabs. +so you have to pay extra attention to indentation. Always use spaces, not tabs. ### Creating a simple `.gitlab-ci.yml` file @@ -107,7 +107,7 @@ If you want to check whether your `.gitlab-ci.yml` file is valid, there is a Lint tool under the page `/ci/lint` of your GitLab instance. You can also find the link under **Settings > CI settings** in your project. -For more information and a complete `.gitlab-ci.yml` syntax, please check +For more information and a complete `.gitlab-ci.yml` syntax, please read [the documentation on .gitlab-ci.yml](../yaml/README.md). ### Push `.gitlab-ci.yml` to GitLab @@ -121,7 +121,8 @@ git commit -m "Add .gitlab-ci.yml" git push origin master ``` -Now if you go to the **Builds** page you will see that the builds are pending. +Now if you go to the **Pipelines** page you will see that the pipeline is +pending. You can also go to the **Commits** page and notice the little clock icon next to the commit SHA. @@ -137,15 +138,14 @@ Notice that there are two jobs pending which are named after what we wrote in `.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured yet for these builds. -The next step is to configure a Runner so that it picks the pending jobs. +The next step is to configure a Runner so that it picks the pending builds. ## Configuring a Runner -In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. -A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker -container or even a cluster of containers. GitLab and the Runners communicate -through an API, so the only needed requirement is that the machine on which the -Runner is configured to have Internet access. +In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. A Runner +can be a virtual machine, a VPS, a bare-metal machine, a docker container or +even a cluster of containers. GitLab and the Runners communicate through an API, +so the only requirement is that the Runner's machine has Internet access. A Runner can be specific to a certain project or serve multiple projects in GitLab. If it serves all projects it's called a _Shared Runner_. @@ -187,12 +187,16 @@ To enable **Shared Runners** you have to go to your project's [Read more on Shared Runners](../runners/README.md). -## Seeing the status of your build +## Seeing the status of your pipeline and builds After configuring the Runner successfully, you should see the status of your last commit change from _pending_ to either _running_, _success_ or _failed_. -You can view all builds, by going to the **Builds** page in your project. +You can view all pipelines by going to the **Pipelines** page in your project. + +![Commit status](img/pipelines_status.png) + +Or you can view all builds, by going to the **Pipelines > Builds** page. ![Commit status](img/builds_status.png) diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png new file mode 100644 index 0000000000000000000000000000000000000000..6bc97bb739cedf27bdebe43a14a1606ff380a2f6 GIT binary patch literal 89387 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%0D$8{MgQ~6 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 literal 0 HcmV?d00001 From edfe2e2db98e1a5541d5d683ac3a686d24a9c429 Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Tue, 14 Jun 2016 20:34:35 -0700 Subject: [PATCH 22/50] Add CD --- doc/README.md | 24 ++++++++++++------------ doc/ci/README.md | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/README.md b/doc/README.md index 5d89d0c9821..3e7165d5ae4 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,17 +3,17 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. -- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples. +- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. +- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. -- [Importing to GitLab](workflow/importing/README.md). +- [Importing to GitLab](workflow/importing/README.md) - [Markdown](markdown/markdown.md) GitLab's advanced formatting system. -- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab +- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](permissions/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. -- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. @@ -24,15 +24,15 @@ external authentication with LDAP, SAML, CAS and additional Omniauth providers. - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough. - [Install](install/README.md) Requirements, directory structures and installation from source. -- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components +- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. -- [Operations](operations/README.md) Keeping GitLab up and running +- [Operations](operations/README.md) Keeping GitLab up and running. - [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects. -- [Repository checks](administration/repository_checks.md) Periodic Git repository checks +- [Repository checks](administration/repository_checks.md) Periodic Git repository checks. - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. @@ -41,11 +41,11 @@ - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. -- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics -- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint -- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs -- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability -- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab +- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics. +- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint. +- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs. +- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability. +- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab. ## Contributor documentation diff --git a/doc/ci/README.md b/doc/ci/README.md index c7a29e269b5..7abce85c8d8 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -1,6 +1,6 @@ ## GitLab CI Documentation -### CI User documentation +### CI/CD User documentation - [Get started with GitLab CI](quick_start/README.md) - [CI/CD Definitions](definitions/README.md) From 41af86bc86a3bab9c83729ff76776fc137b350ef Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Mon, 20 Jun 2016 15:07:38 -0700 Subject: [PATCH 23/50] Move Pipelines and Builds definitions to their own page --- doc/ci/README.md | 4 +-- doc/ci/definitions/README.md | 52 ------------------------------------ doc/ci/environments.md | 2 +- 3 files changed, 3 insertions(+), 55 deletions(-) delete mode 100644 doc/ci/definitions/README.md diff --git a/doc/ci/README.md b/doc/ci/README.md index 7abce85c8d8..f8d02af31ab 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -1,11 +1,11 @@ ## GitLab CI Documentation -### CI/CD User documentation +### CI User documentation - [Get started with GitLab CI](quick_start/README.md) -- [CI/CD Definitions](definitions/README.md) - [CI examples for various languages](examples/README.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) +- [Pipelines](pipelines.md) - [Environments and deployments](environments.md) - [Learn how `.gitlab-ci.yml` works](yaml/README.md) - [Configure a Runner, the application that runs your builds](runners/README.md) diff --git a/doc/ci/definitions/README.md b/doc/ci/definitions/README.md deleted file mode 100644 index 4ec5eee5757..00000000000 --- a/doc/ci/definitions/README.md +++ /dev/null @@ -1,52 +0,0 @@ -## CI/CD Definitions - -### Pipelines - -A pipeline is a group of [builds] that get executed in [stages] (batches). All of -the builds in a stage are executed in parallel (if there are enough concurrent -[runners]), and if they all succeed, the pipeline moves on to the next stage. If -one of the builds fails, the next stage is not (usually) executed. - -### Builds - -Builds are runs of [jobs]. Not to be confused with a `build` stage. - -### Jobs - -Jobs are the basic work unit of CI/CD. Jobs are used to create [builds], which are -then picked up by [Runners] and executed within the environment of the Runner. -Each job is run independently from each other. - -### Runners - -A runner is an isolated (virtual) machine that picks up builds through the -coordinator API of GitLab CI. A runner can be specific to a certain project or -serve any project in GitLab CI. A runner that serves all projects is called a -shared runner. - -### Stages - -Stages allow [jobs] to be grouped into parallel and sequential [builds]. Builds -of the same stage are executed in parallel and builds of the next stage are run -after the jobs from the previous stage complete successfully. Stages allow for -flexible multi-stage [pipelines]. By default [pipelines] have `build`, `test` -and `deploy` stages, but these can be defined in `.gitlab-ci.yml`. If a job -doesn't specify a stage, the job is assigned to the test stage. - -### Environments - -Environments are places where code gets deployed, such as staging or production. -CI/CD [Pipelines] usually have one or more deploy stages with [jobs] that do -[deployments] to an environment. - -### Deployments - -Deployments are created when [jobs] deploy versions of code to [environments]. - -[pipelines]: #pipelines -[builds]: #builds -[runners]: #runners -[jobs]: #jobs -[stages]: #stages -[environments]: #environments -[deployments]: #deployments diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 040379bb381..d85b8a34ced 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -52,7 +52,7 @@ Clicking on an environment will show the history of deployments. Only deploys that happen after your `.gitlab-ci.yml` is properly configured will show up in the environments and deployments lists. -[Pipelines]: quick_start/README.md +[Pipelines]: pipelines.md [jobs]: yaml/README.md#jobs [environments]: #environments [deployments]: #deployments From 2302b2c131044cf502b020d900b334bc187f945a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 20 Jun 2016 15:08:29 -0700 Subject: [PATCH 24/50] Update browser gem to 2.2.0 Fixes https://github.com/fnando/browser/issues/241 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index bc1223e1bbc..2ea91da07fe 100644 --- a/Gemfile +++ b/Gemfile @@ -48,7 +48,7 @@ gem 'attr_encrypted', '~> 3.0.0' gem 'u2f', '~> 0.2.1' # Browser detection -gem "browser", '~> 2.0.3' +gem "browser", '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library diff --git a/Gemfile.lock b/Gemfile.lock index 49e548fb94f..ed01d766e80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,7 +98,7 @@ GEM autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) brakeman (3.3.2) - browser (2.0.3) + browser (2.2.0) builder (3.2.2) bullet (5.0.0) activesupport (>= 3.0.0) @@ -833,7 +833,7 @@ DEPENDENCIES binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) brakeman (~> 3.3.0) - browser (~> 2.0.3) + browser (~> 2.2) bullet bundler-audit byebug From a03cadcdbf3024ec59234a07a9b87884de5cc7dc Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Mon, 20 Jun 2016 15:10:11 -0700 Subject: [PATCH 25/50] Move Pipelines and Builds definitions to their own page --- doc/ci/pipelines.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 doc/ci/pipelines.md diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md new file mode 100644 index 00000000000..48a9f994759 --- /dev/null +++ b/doc/ci/pipelines.md @@ -0,0 +1,38 @@ +# Introduction to pipelines and builds + +>**Note:** +Introduced in GitLab 8.8. + +## Pipelines + +A pipeline is a group of [builds] that get executed in [stages] (batches). All +of the builds in a stage are executed in parallel (if there are enough +concurrent [runners]), and if they all succeed, the pipeline moves on to the +next stage. If one of the builds fails, the next stage is not (usually) +executed. + +## Builds + +Builds are individual runs of [jobs]. Not to be confused with a `build` job or +`build` stage. + +## Defining pipelines + +Pipelines are defined in `.gitlab-ci.yml` by specifying [jobs] that run in +[stages]. + +See full [documentation](yaml/README.md#jobs). + +## Seeing pipeline status + +You can find the current and historical pipeline runs under **Pipelines** for your +project. + +## Seeing build status + +Clicking on a pipeline will show the builds that were run for that pipeline. + +[builds]: #builds +[jobs]: yaml/README.md#jobs +[stages]: yaml/README.md#stages +[runners]: runners/README.md From 0582c085554a8649f3c9daa0dde611e3a851db3e Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Mon, 20 Jun 2016 15:12:04 -0700 Subject: [PATCH 26/50] Add 'and builds' --- doc/ci/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/README.md b/doc/ci/README.md index f8d02af31ab..3dd4e2bc230 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -5,7 +5,7 @@ - [Get started with GitLab CI](quick_start/README.md) - [CI examples for various languages](examples/README.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) -- [Pipelines](pipelines.md) +- [Pipelines and builds](pipelines.md) - [Environments and deployments](environments.md) - [Learn how `.gitlab-ci.yml` works](yaml/README.md) - [Configure a Runner, the application that runs your builds](runners/README.md) From 8888dfaa8f862750aaf4565a41f7941cdf3f1a29 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 21 Jun 2016 13:35:37 +0200 Subject: [PATCH 27/50] add missing import source --- config/initializers/1_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 09ffc319065..c6dc1e4ab38 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -215,7 +215,7 @@ Settings.gitlab.default_projects_features['container_registry'] = true if Settin Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] -Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] +Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project] Settings.gitlab['trusted_proxies'] ||= [] From 59fc1816d3df8f9ac27542eac46f93ae85e328ae Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 21 Jun 2016 07:54:02 -0700 Subject: [PATCH 28/50] Rename Repo -> Repository --- app/views/layouts/nav/_project.html.haml | 4 +-- features/project/active_tab.feature | 30 ++++++++++----------- features/project/shortcuts.feature | 6 ++--- features/steps/project/project_find_file.rb | 4 +-- features/steps/shared/project_tab.rb | 4 +-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 068332205bb..c6f9cbc60e9 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -38,9 +38,9 @@ - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do - = link_to project_files_path(@project), title: 'Repo', class: 'shortcuts-tree' do + = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do %span - Repo + Repository - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :environments]) do diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index db4507b9889..57dda9c2234 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,9 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Repo + Scenario: On Project Repository Given I visit my project's files page - Then the active main tab should be Repo + Then the active main tab should be Repository And no other main tabs should be active Scenario: On Project Issues @@ -59,46 +59,46 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Repo + # Sub Tabs: Repository - Scenario: On Project Repo/Files + Scenario: On Project Repository/Files Given I visit my project's files page Then the active sub tab should be Files And no other sub tabs should be active - And the active main tab should be Repo + And the active main tab should be Repository - Scenario: On Project Repo/Commits + Scenario: On Project Repository/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Repo + And the active main tab should be Repository - Scenario: On Project Repo/Network + Scenario: On Project Repository/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Repo + And the active main tab should be Repository - Scenario: On Project Repo/Compare + Scenario: On Project Repository/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Repo + And the active main tab should be Repository - Scenario: On Project Repo/Branches + Scenario: On Project Repository/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Repo + And the active main tab should be Repository - Scenario: On Project Repo/Tags + Scenario: On Project Repository/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Repo + And the active main tab should be Repository Scenario: On Project Issues/Browse Given I visit my project's issues page diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index aa5edaaa9d7..f71f69ef060 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,21 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Repo + Then the active main tab should be Repository Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Repo + Then the active main tab should be Repository Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Repo + And the active main tab should be Repository @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 9833eec3730..90771847909 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Repo') + ensure_active_main_tab('Repository') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Repo') + ensure_active_main_tab('Repository') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 595913ff3d8..d6024212601 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,8 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Repo' do - ensure_active_main_tab('Repo') + step 'the active main tab should be Repository' do + ensure_active_main_tab('Repository') end step 'the active main tab should be Graphs' do From c89b0d9bf6a65175415ecd734f9053406aa69bca Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 21 Jun 2016 15:55:30 +0100 Subject: [PATCH 29/50] Fixed issue with navbar counts being misaligned Closes #18916 --- app/assets/stylesheets/framework/sidebar.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index a0bb3427af0..98f917ce69b 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -91,7 +91,6 @@ text-decoration: none; font-weight: normal; outline: none; - white-space: nowrap; &:hover, &:active, From d9a4ca5975b4fb91b147930d863f0bb4b9619a64 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 21 Jun 2016 17:07:17 +0200 Subject: [PATCH 30/50] Move pre_process into render_result The method Banzai::Renderer.pre_process would always be called, regardless of whether the Markdown to render was already cached or not. In cache the document _was_ cached the output of the pre-processing pipeline was ignored resulting in it doing nothing but wasting CPU cycles. This commit moves Banzai::Renderer.pre_process into Banzai::Renderer.render_result so that it's _only_ used when needed. --- CHANGELOG | 1 + app/helpers/gitlab_markdown_helper.rb | 2 -- lib/banzai.rb | 4 ---- lib/banzai/renderer.rb | 8 ++------ 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fde2b11d3c9..5790da35c1f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.9.0 (unreleased) - Fix issue with arrow keys not working in search autocomplete dropdown - Fix an issue where note polling stopped working if a window was in the background during a refresh. + - Pre-processing Markdown now only happens when needed - Make EmailsOnPushWorker use Sidekiq mailers queue - Redesign all Devise emails. !4297 - Don't show 'Leave Project' to group members diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index a0dafc52622..1a259656f31 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -50,8 +50,6 @@ module GitlabMarkdownHelper context[:project] ||= @project - text = Banzai.pre_process(text, context) - html = Banzai.render(text, context) context.merge!( diff --git a/lib/banzai.rb b/lib/banzai.rb index b467413a7dd..093382261ae 100644 --- a/lib/banzai.rb +++ b/lib/banzai.rb @@ -7,10 +7,6 @@ module Banzai Renderer.render_result(text, context) end - def self.pre_process(text, context) - Renderer.pre_process(text, context) - end - def self.post_process(html, context) Renderer.post_process(html, context) end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index c14a9c4c722..6718acdef7e 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -30,15 +30,11 @@ module Banzai end def self.render_result(text, context = {}) + text = Pipeline[:pre_process].to_html(text, context) if text + Pipeline[context[:pipeline]].call(text, context) end - def self.pre_process(text, context) - pipeline = Pipeline[:pre_process] - - pipeline.to_html(text, context) - end - # Perform post-processing on an HTML String # # This method is used to perform state-dependent changes to a String of From c2d6981db4d6877821674357208ceed9a9a132c8 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 21 Jun 2016 11:02:53 -0500 Subject: [PATCH 31/50] Capitalize button text --- app/views/projects/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 5f041aedfc0..15f0d85194b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -60,7 +60,7 @@ - unless @repository.gitlab_ci_yml %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set up CI + Set Up CI - if @repository.commit .content-block.second-block.white From 8e84153044059da618b6238da7b37fc627c3140d Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 15 Jun 2016 08:54:31 -0500 Subject: [PATCH 32/50] Style sub nav links in Graphs --- app/views/projects/graphs/_head.html.haml | 26 +++--- app/views/projects/graphs/ci.html.haml | 25 +++--- app/views/projects/graphs/commits.html.haml | 90 ++++++++++--------- app/views/projects/graphs/languages.html.haml | 36 ++++---- app/views/projects/graphs/show.html.haml | 44 ++++----- 5 files changed, 116 insertions(+), 105 deletions(-) diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 8becaea246f..38af1459e8b 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,12 +1,14 @@ -- page_specific_javascripts asset_path("graphs/application.js") -%ul.nav-links - = nav_link(action: :show) do - = link_to 'Contributors', namespace_project_graph_path - = nav_link(action: :commits) do - = link_to 'Commits', commits_namespace_project_graph_path - = nav_link(action: :languages) do - = link_to 'Languages', languages_namespace_project_graph_path - - if @project.builds_enabled? - = nav_link(action: :ci) do - = link_to ci_namespace_project_graph_path do - Continuous Integration +%ul.nav-links.sub-nav + %div{ class: (container_class) } + + - page_specific_javascripts asset_path("graphs/application.js") + = nav_link(action: :show) do + = link_to 'Contributors', namespace_project_graph_path + = nav_link(action: :commits) do + = link_to 'Commits', commits_namespace_project_graph_path + = nav_link(action: :languages) do + = link_to 'Languages', languages_namespace_project_graph_path + - if @project.builds_enabled? + = nav_link(action: :ci) do + = link_to ci_namespace_project_graph_path do + Continuous Integration diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 19ccc125ea8..6a40cff0e09 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,15 +1,18 @@ +- @no_container = true - page_title "Continuous Integration", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .oneline - A collection of graphs for Continuous Integration -#charts.ci-charts - .row - .col-md-6 - = render 'projects/graphs/ci/overall' - .col-md-6 - = render 'projects/graphs/ci/build_times' +%div{ class: (container_class) } + .row-content-block.append-bottom-default + .oneline + A collection of graphs for Continuous Integration - %hr - = render 'projects/graphs/ci/builds' + #charts.ci-charts + .row + .col-md-6 + = render 'projects/graphs/ci/overall' + .col-md-6 + = render 'projects/graphs/ci/build_times' + + %hr + = render 'projects/graphs/ci/builds' diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index d9b2fb6c065..2b858c5e0f6 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,52 +1,54 @@ +- @no_container = true - page_title "Commits", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs_commits' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs +%div{ class: (container_class) } + .row-content-block.append-bottom-default + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs_commits' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs -%p.lead - Commit statistics for - %strong #{@ref} - #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} + %p.lead + Commit statistics for + %strong #{@ref} + #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} -.row - .col-md-6 - %ul - %li - %p.lead - %strong #{@commits_graph.commits.size} - commits during - %strong #{@commits_graph.duration} - days - %li - %p.lead - Average - %strong #{@commits_graph.commit_per_day} - commits per day - %li - %p.lead - Contributed by - %strong #{@commits_graph.authors} - authors - .col-md-6 - %div - %p.slead - Commits per day of month - %canvas#month-chart -.row - .col-md-6 - %div - %p.slead - Commits per day hour (UTC) - %canvas#hour-chart - .col-md-6 - %div - %p.slead - Commits per weekday - %canvas#weekday-chart + .row + .col-md-6 + %ul + %li + %p.lead + %strong #{@commits_graph.commits.size} + commits during + %strong #{@commits_graph.duration} + days + %li + %p.lead + Average + %strong #{@commits_graph.commit_per_day} + commits per day + %li + %p.lead + Contributed by + %strong #{@commits_graph.authors} + authors + .col-md-6 + %div + %p.slead + Commits per day of month + %canvas#month-chart + .row + .col-md-6 + %div + %p.slead + Commits per day hour (UTC) + %canvas#hour-chart + .col-md-6 + %div + %p.slead + Commits per weekday + %canvas#weekday-chart :javascript var responsiveChart = function (selector, data) { diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml index 249c16f4709..415414a4ebb 100644 --- a/app/views/projects/graphs/languages.html.haml +++ b/app/views/projects/graphs/languages.html.haml @@ -1,24 +1,26 @@ +- @no_container = true - page_title "Languages", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .oneline - Programming languages used in this repository +%div{ class: (container_class) } + .row-content-block.append-bottom-default + .oneline + Programming languages used in this repository -.row - .col-md-8 - %canvas#languages-chart{ height: 400 } - .col-md-4 - %ul.bordered-list - - @languages.each do |language| - %li - %span{ style: "color: #{language[:color]}" } - = icon('circle') -   - = language[:label] - .pull-right - = language[:value] - \% + .row + .col-md-8 + %canvas#languages-chart{ height: 400 } + .col-md-4 + %ul.bordered-list + - @languages.each do |language| + %li + %span{ style: "color: #{language[:color]}" } + = icon('circle') +   + = language[:label] + .pull-right + = language[:value] + \% :javascript var data = #{@languages.to_json}; diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 33970e7b909..bdfde634f7c 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,29 +1,31 @@ +- @no_container = true - page_title "Contributors", "Graphs" = render 'head' -.row-content-block.append-bottom-default - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs +%div{ class: (container_class) } + .row-content-block.append-bottom-default + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs -.loading-graph - .center - %h3.page-title - %i.fa.fa-spinner.fa-spin - Building repository graph. - %p.slead Please wait a moment, this page will automatically refresh when ready. + .loading-graph + .center + %h3.page-title + %i.fa.fa-spinner.fa-spin + Building repository graph. + %p.slead Please wait a moment, this page will automatically refresh when ready. -.stat-graph.hide - .header.clearfix - %h3#date_header.page-title - %p.light - Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. - %input#brush_change{:type => "hidden"} - .graphs - #contributors-master - #contributors.clearfix - %ol.contributors-list.clearfix + .stat-graph.hide + .header.clearfix + %h3#date_header.page-title + %p.light + Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. + %input#brush_change{:type => "hidden"} + .graphs + #contributors-master + #contributors.clearfix + %ol.contributors-list.clearfix From a4c9b4fbe9cc6e0e91d43137db71d8d15d22e09a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 15 Jun 2016 09:31:41 -0500 Subject: [PATCH 33/50] Style wiki sub nav links --- app/views/projects/graphs/_head.html.haml | 4 +- app/views/projects/wikis/_nav.html.haml | 12 ++--- app/views/projects/wikis/_new.html.haml | 31 ++++++----- app/views/projects/wikis/edit.html.haml | 28 +++++----- app/views/projects/wikis/git_access.html.haml | 52 ++++++++++--------- app/views/projects/wikis/pages.html.haml | 18 ++++--- app/views/projects/wikis/show.html.haml | 34 ++++++------ 7 files changed, 95 insertions(+), 84 deletions(-) diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 38af1459e8b..a388d9a0a61 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,5 +1,5 @@ -%ul.nav-links.sub-nav - %div{ class: (container_class) } +.nav-links.sub-nav + %ul{ class: (container_class) } - page_specific_javascripts asset_path("graphs/application.js") = nav_link(action: :show) do diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 988fe024e28..f82c0fc0aff 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,5 +1,5 @@ -.top-area - %ul.nav-links +.nav-links.sub-nav + %ul{ class: (container_class) } = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) @@ -10,9 +10,9 @@ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do Git Access - .nav-controls - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New Page + = nav_link do + = link_to '#modal-new-wiki', "data-toggle" => "modal" do + New Page -= render 'projects/wikis/new' + = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 919daf0a7b2..4f8abcdc8e1 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -1,14 +1,17 @@ -%div#modal-new-wiki.modal - .modal-dialog - .modal-content - .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title New Wiki Page - .modal-body - %form.new-wiki-page - .form-group - = label_tag :new_wiki_path do - %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true - .form-actions - = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' +- @no_container = true + +%div{ class: (container_class) } + %div#modal-new-wiki.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title New Wiki Page + .modal-body + %form.new-wiki-page + .form-group + = label_tag :new_wiki_path do + %span Page slug + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true + .form-actions + = button_tag 'Create Page', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index cbd69ee1a73..81cb709d053 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,19 +1,21 @@ +- @no_container = true - page_title "Edit", @page.title.capitalize, "Wiki" = render 'nav' -.top-area - .nav-text.wiki-page - %strong - - if @page.persisted? - = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - - else - = @page.title.capitalize - %span.light - · - Edit Page +%div{ class: (container_class) } + .top-area + .nav-text.wiki-page + %strong + - if @page.persisted? + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + - else + = @page.title.capitalize + %span.light + · + Edit Page - .nav-controls - = render 'main_links' + .nav-controls + = render 'main_links' -= render 'form' + = render 'form' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index ccceab6155e..e129df16123 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,32 +1,34 @@ +- @no_container = true - page_title "Git Access", "Wiki" = render 'nav' -.row-content-block - %span.oneline - Git access for - %strong= @project_wiki.path_with_namespace +%div{ class: (container_class) } + .row-content-block + %span.oneline + Git access for + %strong= @project_wiki.path_with_namespace - .pull-right - = render "shared/clone_panel", project: @project_wiki + .pull-right + = render "shared/clone_panel", project: @project_wiki -.git-empty.prepend-top-default - %fieldset - %legend Install Gollum: - %pre.dark - :preserve - gem install gollum + .git-empty.prepend-top-default + %fieldset + %legend Install Gollum: + %pre.dark + :preserve + gem install gollum - %legend Clone Your Wiki: - %pre.dark - :preserve - git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} - cd #{h @project_wiki.path} + %legend Clone Your Wiki: + %pre.dark + :preserve + git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} + cd #{h @project_wiki.path} - %legend Start Gollum And Edit Locally: - %pre.dark - :preserve - gollum - == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin - >> Thin web server (v1.5.0 codename Knife) - >> Maximum connections set to 1024 - >> Listening on 0.0.0.0:4567, CTRL+C to stop + %legend Start Gollum And Edit Locally: + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 2f6162fa3c5..81d9f391c1c 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,12 +1,14 @@ +- @no_container = true - page_title "Pages", "Wiki" = render 'nav' -%ul.content-list - - @wiki_pages.each do |wiki_page| - %li - = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) - %small (#{wiki_page.format}) - .pull-right - %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} -= paginate @wiki_pages, theme: 'gitlab' +%div{ class: (container_class) } + %ul.content-list + - @wiki_pages.each do |wiki_page| + %li + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) + %small (#{wiki_page.format}) + .pull-right + %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} + = paginate @wiki_pages, theme: 'gitlab' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 9166c0edb3b..76f9b1ecd76 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,24 +1,26 @@ +- @no_container = true - page_title @page.title.capitalize, "Wiki" = render 'nav' -.top-area - .nav-text - %strong= @page.title.capitalize +%div{ class: (container_class) } + .top-area + .nav-text + %strong= @page.title.capitalize - %span.wiki-last-edit-by - · - last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + %span.wiki-last-edit-by + · + last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} - .nav-controls - = render 'main_links' + .nav-controls + = render 'main_links' -- if @page.historical? - .warning_message - This is an old version of this page. - You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. + - if @page.historical? + .warning_message + This is an old version of this page. + You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. -.wiki-holder.prepend-top-default.append-bottom-default - .wiki - = preserve do - = render_wiki_content(@page) + .wiki-holder.prepend-top-default.append-bottom-default + .wiki + = preserve do + = render_wiki_content(@page) From 596b2f46c835e76748a3026ea080323406ad461c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 15 Jun 2016 09:37:36 -0500 Subject: [PATCH 34/50] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0c3aadc29d1..19edf7c75f9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -148,6 +148,7 @@ v 8.9.0 (unreleased) - Ensure Todos counters doesn't count Todos for projects pending delete - Add left/right arrows horizontal navigation - Add tooltip to pin/unpin navbar + - Add new sub nav style to Wiki and Graphs sub navigation v 8.8.5 - Import GitHub repositories respecting the API rate limit !4166 From a02c3d09d3670c7b7543461bf4d3c4d5fc08f418 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 15 Jun 2016 12:01:58 -0500 Subject: [PATCH 35/50] Fix graph columns --- app/assets/stylesheets/pages/stat_graph.scss | 17 +++++++++++++---- app/views/projects/graphs/show.html.haml | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 85a0304196c..4adf6193b8a 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -14,18 +14,27 @@ font-size: 10px; } +#contributors-master { + @include make-md-column(12); + + svg { + width: 100%; + } +} + #contributors { .contributors-list { margin: 0 0 10px; list-style: none; padding: 0; + + svg { + width: 100%; + } } .person { - &:nth-child(even) { - float: right; - } - float: left; + @include make-md-column(6); margin-top: 10px; } diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index bdfde634f7c..61706c1d0fe 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -22,7 +22,7 @@ %p.light Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. %input#brush_change{:type => "hidden"} - .graphs + .graphs.row #contributors-master #contributors.clearfix %ol.contributors-list.clearfix From 6f642fd423f32a05711e8cf97d79b57cd6e489f3 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 16 Jun 2016 09:09:58 -0500 Subject: [PATCH 36/50] Add new style for sub header block info --- app/assets/stylesheets/framework/blocks.scss | 11 +++++++++++ app/assets/stylesheets/framework/nav.scss | 4 ---- app/views/projects/graphs/ci.html.haml | 2 +- app/views/projects/graphs/commits.html.haml | 2 +- app/views/projects/graphs/languages.html.haml | 2 +- app/views/projects/graphs/show.html.haml | 2 +- app/views/projects/wikis/edit.html.haml | 2 +- app/views/projects/wikis/git_access.html.haml | 4 ++-- 8 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d5fe5bc2ef1..7626ac41e50 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -97,6 +97,17 @@ } } +.sub-header-block { + background-color: $white-light; + border-bottom: 1px solid $white-dark; + margin: 11px 0; + padding-bottom: 11px; + + .oneline { + line-height: 35px; + } +} + .cover-block { text-align: center; background: $background-color; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 694f09c0464..0281b06d3ba 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -111,10 +111,6 @@ width: 50%; line-height: 28px; - &.wiki-page { - padding: 16px 10px 11px; - } - /* Small devices (phones, tablets, 768px and lower) */ @media (max-width: $screen-sm-min) { width: 100%; diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 6a40cff0e09..e695d3ae369 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -3,7 +3,7 @@ = render 'head' %div{ class: (container_class) } - .row-content-block.append-bottom-default + .sub-header-block .oneline A collection of graphs for Continuous Integration diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 2b858c5e0f6..0daffe68f6f 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -3,7 +3,7 @@ = render 'head' %div{ class: (container_class) } - .row-content-block.append-bottom-default + .sub-header-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'graphs_commits' %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml index 415414a4ebb..6d97f552a8e 100644 --- a/app/views/projects/graphs/languages.html.haml +++ b/app/views/projects/graphs/languages.html.haml @@ -3,7 +3,7 @@ = render 'head' %div{ class: (container_class) } - .row-content-block.append-bottom-default + .sub-header-block .oneline Programming languages used in this repository diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 61706c1d0fe..9f7e2a361ff 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -3,7 +3,7 @@ = render 'head' %div{ class: (container_class) } - .row-content-block.append-bottom-default + .sub-header-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'graphs' %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 81cb709d053..af9d23bbfb3 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -4,7 +4,7 @@ %div{ class: (container_class) } .top-area - .nav-text.wiki-page + .nav-text %strong - if @page.persisted? = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index e129df16123..6caf7230f35 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -3,7 +3,7 @@ = render 'nav' %div{ class: (container_class) } - .row-content-block + .sub-header-block %span.oneline Git access for %strong= @project_wiki.path_with_namespace @@ -11,7 +11,7 @@ .pull-right = render "shared/clone_panel", project: @project_wiki - .git-empty.prepend-top-default + .prepend-top-default %fieldset %legend Install Gollum: %pre.dark From 1d7b809a01fcd4479191443598c364c367f17545 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 16 Jun 2016 09:57:42 -0500 Subject: [PATCH 37/50] Move new page link out of sub nav --- app/assets/stylesheets/framework/blocks.scss | 4 ++-- app/views/projects/wikis/_main_links.html.haml | 3 +++ app/views/projects/wikis/_nav.html.haml | 5 ----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 7626ac41e50..4e0a788123d 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -100,8 +100,8 @@ .sub-header-block { background-color: $white-light; border-bottom: 1px solid $white-dark; - margin: 11px 0; - padding-bottom: 11px; + padding: 11px 0; + margin-bottom: 11px; .oneline { line-height: 35px; diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 4faa547769b..4ea75dbbf0c 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,4 +1,7 @@ - if (@page && @page.persisted?) + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New Page = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do Page History - if can?(current_user, :create_wiki, @project) diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index f82c0fc0aff..f8ea479e0b1 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -10,9 +10,4 @@ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do Git Access - - if can?(current_user, :create_wiki, @project) - = nav_link do - = link_to '#modal-new-wiki', "data-toggle" => "modal" do - New Page - = render 'projects/wikis/new' From 25ae737df0d8e3af9d40b60896ecb906237142ac Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 16 Jun 2016 12:21:23 -0500 Subject: [PATCH 38/50] Add new page button to wiki home --- app/views/projects/wikis/edit.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index af9d23bbfb3..bf5d09d50c2 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -15,6 +15,9 @@ Edit Page .nav-controls + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New Page = render 'main_links' From 1f7353ce38c2bcc957d10c35aadc4197668cc7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 21 Jun 2016 18:25:44 +0200 Subject: [PATCH 39/50] Fix specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- features/admin/groups.feature | 7 ------- features/steps/admin/groups.rb | 8 +------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/features/admin/groups.feature b/features/admin/groups.feature index ab7de7ac315..657e847cf4a 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -26,13 +26,6 @@ Feature: Admin Groups When I visit group page Then I should see project shared with group - @javascript - Scenario: Remove user from group - Given we have user "John Doe" in group - When I visit admin group page - And I remove user "John Doe" from group - Then I should not see "John Doe" in team list - @javascript Scenario: Invite user to a group by e-mail When I visit admin group page diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index e1f1db2872f..8613dc537cc 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -62,7 +62,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do page.within ".group-users-list" do - expect(page).to have_content "johndoe@gitlab.com (invited)" + expect(page).to have_content "johndoe@gitlab.com – Invited by" expect(page).to have_content "Reporter" end end @@ -92,12 +92,6 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps current_group.add_reporter(user_john) end - step 'I remove user "John Doe" from group' do - page.within "#user_#{user_john.id}" do - click_link 'Remove user from group' - end - end - step 'I should not see "John Doe" in team list' do page.within ".group-users-list" do expect(page).not_to have_content "John Doe" From 835e3b0ac66aa24e1e087c1cf87308efd2d22527 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 21 Jun 2016 16:47:29 +0000 Subject: [PATCH 40/50] Update mail_room to 0.8.0 to resolve #13357 Which includes the fix from: https://github.com/tpitale/mail_room/pull/73 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index bc1223e1bbc..50a47bfd984 100644 --- a/Gemfile +++ b/Gemfile @@ -330,7 +330,7 @@ gem "newrelic_rpm", '~> 3.14' gem 'octokit', '~> 4.3.0' -gem "mail_room", "~> 0.7" +gem "mail_room", "~> 0.8" gem 'email_reply_parser', '~> 0.5.8' diff --git a/Gemfile.lock b/Gemfile.lock index 49e548fb94f..d20514e79e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -398,7 +398,7 @@ GEM systemu (~> 2.6.2) mail (2.6.4) mime-types (>= 1.16, < 4) - mail_room (0.7.0) + mail_room (0.8.0) method_source (0.8.2) mime-types (2.99.2) mimemagic (0.3.0) @@ -899,7 +899,7 @@ DEPENDENCIES license_finder licensee (~> 8.0.0) loofah (~> 2.0.3) - mail_room (~> 0.7) + mail_room (~> 0.8) method_source (~> 0.8) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) From b2f60bb9a118c366ad462241bd4842c52d872d5f Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 21 Jun 2016 17:54:41 +0100 Subject: [PATCH 41/50] Fix auto-MR-close text from branch name Rails's form helpers use the `$attr_before_type_cast` method where available, and this value only appears to be updated on assignment, not when the object is mutated in some other way: [1] pry(main)> mr = MergeRequest.new => # [2] pry(main)> mr.description = 'foo' => "foo" [3] pry(main)> mr.description << ' bar' => "foo bar" [4] pry(main)> mr.description => "foo bar" [5] pry(main)> mr.description_before_type_cast => "foo" [6] pry(main)> mr.description += ' bar' => "foo bar bar" [7] pry(main)> mr.description_before_type_cast => "foo bar bar" --- CHANGELOG | 1 + app/services/merge_requests/build_service.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 0c3aadc29d1..374cc971ef3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.9.0 (unreleased) - Fix endless redirections when accessing user OAuth applications when they are disabled - Allow enabling wiki page events from Webhook management UI - Bump rouge to 1.11.0 + - Fix MR-auto-close text added to description - Fix issue with arrow keys not working in search autocomplete dropdown - Fix an issue where note polling stopped working if a window was in the background during a refresh. diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 1b48899bb0a..7fe57747265 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -83,7 +83,7 @@ module MergeRequests closes_issue = "Closes ##{iid}" if merge_request.description.present? - merge_request.description << closes_issue.prepend("\n") + merge_request.description += closes_issue.prepend("\n") else merge_request.description = closes_issue end From 4b1e505ef8dc55d719c5dccfde75acff537e01b6 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 21 Jun 2016 19:21:13 +0200 Subject: [PATCH 42/50] Add missing link to sidekiq metrics in API README [ci skip] --- doc/api/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/README.md b/doc/api/README.md index 73f44603688..288f7f9ee69 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -32,6 +32,7 @@ following locations: - [Services](services.md) - [Session](session.md) - [Settings](settings.md) +- [Sidekiq metrics](sidekiq_metrics.md) - [System Hooks](system_hooks.md) - [Tags](tags.md) - [Users](users.md) From 8b8cef4ca505c4787829d1c61efda4ad6f8608d5 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Mon, 20 Jun 2016 15:38:02 -0700 Subject: [PATCH 43/50] Add documentation and examples for configuring cloud storage for registry images. --- doc/administration/container_registry.md | 75 +++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 7870669fa77..de225385c7a 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -22,6 +22,7 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr - [Disable Container Registry per project](#disable-container-registry-per-project) - [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide) - [Container Registry storage path](#container-registry-storage-path) +- [Container Registry storage driver](#container-registry-storage-driver) - [Storage limitations](#storage-limitations) - [Changelog](#changelog) @@ -306,8 +307,12 @@ the Container Registry by themselves, follow the steps below. ## Container Registry storage path -To change the storage path where Docker images will be stored, follow the -steps below. +>**Note:** +For configuring storage in the cloud instead of the filesystem, see the +[storage driver configuration](#container-registry-storage-driver). + +If you want to store your images on the filesystem, you can change the storage +path for the Container Registry, follow the steps below. This path is accessible to: @@ -349,6 +354,72 @@ The default location where images are stored in source installations, is 1. Save the file and [restart GitLab][] for the changes to take effect. +## Container Registry storage driver + +You can configure the Container Registry to use a different storage backend by +configuring a different storage driver. By default the GitLab Container Registry +is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path) +configuration. + +The different supported drivers are: + +| Driver | Description | +|------------|-------------------------------------| +| filesystem | Uses a path on the local filesystem | +| azure | Microsoft Azure Blob Storage | +| gcs | Google Cloud Storage | +| s3 | Amazon Simple Storage Service | +| swift | OpenStack Swift Object Storage | +| oss | Aliyun OSS | + +Read more about the individual driver's config options in the +[Docker Registry docs][storage-config]. + +> **Warning** GitLab will not backup Docker images that are not stored on the +filesystem. Remember to enable backups with your object storage provider if +desired. + +--- + +**Omnibus GitLab installations** + +To configure the storage driver in Omnibus: + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + registry['storage'] = { + 's3' => { + 'accesskey' => 's3-access-key', + 'secretkey' => 's3-secret-key-for-access-key', + 'bucket' => 'your-s3-bucket' + } + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +Configuring the storage driver is done in your registry config YML file created +when you [deployed your docker registry][registry-deploy]. + +Example: + +``` +storage: + s3: + accesskey: 'AKIAKIAKI' + secretkey: 'secret123' + bucket: 'gitlab-registry-bucket-AKIAKIAKI' + cache: + blobdescriptor: inmemory + delete: + enabled: true +``` + ## Storage limitations Currently, there is no storage limitation, which means a user can upload an From 795b8b3073c3ee65921ec01b5e1cd2524705a7ef Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 21 Jun 2016 13:16:28 -0500 Subject: [PATCH 44/50] Update gray block under subnav to match other pages --- app/assets/stylesheets/framework/blocks.scss | 16 +++++++++ app/views/projects/compare/index.html.haml | 2 +- app/views/projects/compare/show.html.haml | 36 ++++++++++---------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d5fe5bc2ef1..38023818709 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -97,6 +97,22 @@ } } +.sub-header-block { + background-color: $white-light; + border-bottom: 1px solid $white-dark; + padding: 11px 0; + margin-bottom: 11px; + + .oneline { + line-height: 35px; + } + + &.no-bottom-space { + border-bottom: 0; + margin-bottom: 0; + } +} + .cover-block { text-align: center; background: $background-color; diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index c322942aeba..b22285c11e0 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -3,7 +3,7 @@ = render "projects/commits/head" %div{ class: (container_class) } - .row-content-block.second-block.content-component-block + .sub-header-block Compare branches, tags or commit ranges. %br Fill input field with commit id like diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index cdc34f51d6d..f4ec7b767f6 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,24 +1,24 @@ +- @no_container = true - page_title "#{params[:from]}...#{params[:to]}" = render "projects/commits/head" +%div{ class: (container_class) } + .sub-header-block.no-bottom-space + = render "form" -.row-content-block - = render "form" - -- if @commits.present? - .prepend-top-default + - if @commits.present? = render "projects/commits/commit_list" = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs -- else - .light-well.prepend-top-default - .center - %h4 - There isn't anything to compare. - %p.slead - - if params[:to] == params[:from] - %span.label-branch #{params[:from]} - and - %span.label-branch #{params[:to]} - are the same. - - else - You'll need to use different branch names to get a valid comparison. + - else + .light-well + .center + %h4 + There isn't anything to compare. + %p.slead + - if params[:to] == params[:from] + %span.label-branch #{params[:from]} + and + %span.label-branch #{params[:to]} + are the same. + - else + You'll need to use different branch names to get a valid comparison. From 07b3bf9ec3900324195566c0d4aeb05abfdb9277 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 21 Jun 2016 10:00:08 -0600 Subject: [PATCH 45/50] Fix Network graph links. Resolves #18894. --- app/views/projects/network/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index e4ab064eda8..de767e1979d 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -15,5 +15,5 @@ = check_box_tag :filter_ref, 1, @options[:filter_ref] %span Begin with the selected commit - .network-graph{ data: { url: '#{escape_javascript(@url)}', commit_url: '#{escape_javascript(@commit_url)}', ref: '#{escape_javascript(@ref)}', commit_id: '#{escape_javascript(@commit.id)}' } } + .network-graph{ data: { url: "#{@url}", commit_url: "#{@commit_url}", ref: "#{@ref}", commit_id: "#{@commit.id}" } } = spinner nil, true From 91a112cba452857a80233986be359a3a4f9a288c Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 21 Jun 2016 12:35:55 -0600 Subject: [PATCH 46/50] Remove unnecessary string interpolation. --- app/views/projects/network/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index de767e1979d..593af319a47 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -15,5 +15,5 @@ = check_box_tag :filter_ref, 1, @options[:filter_ref] %span Begin with the selected commit - .network-graph{ data: { url: "#{@url}", commit_url: "#{@commit_url}", ref: "#{@ref}", commit_id: "#{@commit.id}" } } + .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } } = spinner nil, true From 76c6b9c8d87f8e89b046becdfa055845642aef4a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 21 Jun 2016 23:54:00 +0300 Subject: [PATCH 47/50] Put wiki page history content into container Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/wikis/history.html.haml | 66 +++++++++++----------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 45460ed9f41..630ee35b70b 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,37 +1,37 @@ - page_title "History", @page.title.capitalize, "Wiki" = render 'nav' +%div{ class: (container_class) } + .top-area + .nav-text + %strong + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + %span.light + · + History -.top-area - .nav-text - %strong - = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - %span.light - · - History - -.table-holder - %table.table - %thead - %tr - %th Page version - %th Author - %th Commit Message - %th Last updated - %th Format - %tbody - - @page.versions.each_with_index do |version, index| - - commit = version + .table-holder + %table.table + %thead %tr - %td - = link_to project_wiki_path_with_version(@project, @page, - commit.id, index == 0) do - = truncate_sha(commit.id) - %td - = commit.author.name - %td - = commit.message - %td - #{time_ago_with_tooltip(version.authored_date)} - %td - %strong - = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + %th Page version + %th Author + %th Commit Message + %th Last updated + %th Format + %tbody + - @page.versions.each_with_index do |version, index| + - commit = version + %tr + %td + = link_to project_wiki_path_with_version(@project, @page, + commit.id, index == 0) do + = truncate_sha(commit.id) + %td + = commit.author.name + %td + = commit.message + %td + #{time_ago_with_tooltip(version.authored_date)} + %td + %strong + = @page.page.wiki.page(@page.page.name, commit.id).try(:format) From 82b32c286ab519b94d6b5d5467b05b43b9b31fc9 Mon Sep 17 00:00:00 2001 From: Mark Pundsack Date: Tue, 21 Jun 2016 22:15:21 -0700 Subject: [PATCH 48/50] Fix link --- doc/ci/quick_start/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index c32b69aad8b..7fa1a478f34 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -241,4 +241,4 @@ CI with various languages. [runner]: ../runners/README.md [enabled]: ../enable_or_disable_ci.md [stages]: ../yaml/README.md#stages -[pipeline]: ../definitions/README.md#pipelines +[pipeline]: ../pipelines.md From d19379e00d95a3153bfe608ba0404cdbf82d5246 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 22 Jun 2016 07:39:08 +0200 Subject: [PATCH 49/50] fix typo --- app/views/notify/project_was_not_exported_email.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/notify/project_was_not_exported_email.html.haml b/app/views/notify/project_was_not_exported_email.html.haml index c9e9ade2cf1..c888da29c17 100644 --- a/app/views/notify/project_was_not_exported_email.html.haml +++ b/app/views/notify/project_was_not_exported_email.html.haml @@ -6,4 +6,4 @@ %ul - @errors.each do |error| %li - error + #{error} From 5adc08596287ccb28e0b6489896b71d4943750ae Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 22 Jun 2016 08:48:18 +0200 Subject: [PATCH 50/50] refactor to use haml instead --- app/views/notify/project_was_not_exported_email.text.erb | 6 ------ app/views/notify/project_was_not_exported_email.text.haml | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 app/views/notify/project_was_not_exported_email.text.erb create mode 100644 app/views/notify/project_was_not_exported_email.text.haml diff --git a/app/views/notify/project_was_not_exported_email.text.erb b/app/views/notify/project_was_not_exported_email.text.erb deleted file mode 100644 index a07f6edacf7..00000000000 --- a/app/views/notify/project_was_not_exported_email.text.erb +++ /dev/null @@ -1,6 +0,0 @@ -Project <%= @project.name %> couldn't be exported. - -The errors we encountered were: - -- @errors.each do |error| -<%= error %> \ No newline at end of file diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml new file mode 100644 index 00000000000..b27cb620b9e --- /dev/null +++ b/app/views/notify/project_was_not_exported_email.text.haml @@ -0,0 +1,6 @@ += "Project #{@project.name} couldn't be exported." + += "The errors we encountered were:" + +- @errors.each do |error| + #{error} \ No newline at end of file